ggplot2_封面

ggplot2入门与进阶(上)

    ggplot2包是基于Wilkinson在《Grammar of Graphics》一书中所提出的图形语法的具体实现, 这套图形语法把绘图过程归纳为data, transformation, scale, coordinates, elements, guides, display等一系列独立的步骤, 通过将这些步骤搭配组合, 来实现个性化的统计绘图。于是, 得益于该图形语法, Hadley Wickham所开发的ggplot2是如此人性化, 不同于R基础绘图和先前的lattice那样参数满天飞, 而是摈弃了诸多繁琐细节, 并以人的思维进行高质量作图。在ggplot2包中, 加号的引入革命性的, 这个神奇的符号完成了一系列图形语法叠加, 也是这个符号, 让很多人喜欢上了用R来进行统计绘图。

    ggplot2的作者Hadley Wickham绝对是R界的男神, 很多人都对Hadley的R包重度依赖, 例如他所开发的plyr、reshape2和ggplot2三款包, 完全可满足日常的数据处理和可视化过程。所以, 当男神今年造访北京R语言大会引起了不小的轰动。

    在论文绘图哪家强这个问题上, 知乎上已有各种神回复。除却一些稀奇古怪的绘图难以达成, ggplot2以美轮美奂的统计制图能力一定能排进各类绘图软件的前三。对ggplot2功能的扩展也在加强。例如plotly能够支持以ggplot2为源码的交互式绘图, Hadley正在开发ggvis除了支持交互式绘图, 已把管道作为ggvis的重要组成部分。可以预见的是统计绘图将会变得更交互、更快速。

    本文既是自己对ggplot2学习的总结, 也希望能帮助大家对ggplot2快速入门。主要以Hadley的官方文档为教材, 介绍了ggplot2的基本原理和操作, 链接了部分ggplot2参数索引, 并在最后加入了作图实战。

ggplot2的图形语法

    尽管qplot作为ggplot2的快速作图(quick plot)函数, 能够极大的简化作图步骤, 容易入门和上手, 但是qplot却不是泛型函数, 而ggplot()作为泛型函数, 能对任意类型的R对象进行可视化操作, 是ggplot2的精髓所在, 因而在本文中主要的绘图都是通过ggplot()来完成的。有关于qplot的介绍可以细看Hadley的官方介绍

    在Hadley的ggplot2官方文档中, Hadely这样对Wilkinson的图形语法进行了描述:“一张统计图形就是从数据到几何对象(geometric object, 缩写为geom, 包括点、线、条形等)的图形属性(aesthetic attributes, 缩写为aes, 包括颜色、形状、大小等)的一个映射。此外, 图形中还可能包含数据的统计变换(statistical transformation, 缩写为stats), 最后绘制在某个特定的坐标系(coordinate system,  缩写为coord)中, 而分面(facet, 指将绘图窗口划分为若干个子窗口)则可以用来生成数据中不同子集的图形。”因此在ggplot2中, 图形语法中至少包括了如下几个图形部件:

  1. 数据(data)
  2. 映射(mapping)
  3. 几何对象(geom)
  4. 统计变换(stats)
  5. 标度(scale)
  6. 坐标系(coord)
  7. 分面(facet)

    这些组件之间是通过“+”, 以图层(layer)的方式来粘合构图的, 所以图层是ggplot2中一个重要的概念。当然, 在掌握基本的图形部件基础上, 要完成一幅高质量的统计绘图, 仍然需要其他图形部件来进一步扩展, 这包括了:

  1. 主题(theme)
  2. 存储和输出

    接下来将对这些在ggplot2包中出现的概念逐一展开

数据(data)

  1. 在ggplot2中, 所接受的数据集必须为数据框(data.frame)格式, 如内置的mtcars数据集:
head(mtcars)
  1. 这种格式带来的好处是数据易于存储, 也能在保留原有的绘图参数下, 用%+%方便地变更已有数据集。

为通过”+”以图层的方式加入点的几何对象 

p <- ggplot(mtcars, aes(mpg, wt, colour = cyl)) + 
  geom_point() #geom_point()为通过”+”以图层的方式加入点的几何对象
p
mtcars.c <- transform(mtcars, mpg = mpg^2)
p %+% mtcars.c #用mtcars.c替换mtcars

 

 

 

 

 

 

 

  1. 而ggplot2进行数据分组时必须根据行, 而不能根据列, 例如在mtcars的数据集中, 可以把汽车按汽缸数进行分组, 但不能按汽车的档位数和汽缸数这两个变量分为两组。这要求把“宽”数据转化为“长”数据。所谓的长数据是变量不在是放在各个列上, 而是拍成一列, 每一个变量都分别占其中的几行, 这样就能方便的对每个变量进行分组。reshape2中melt()和cast()能够灵活的融合(melt)重铸(cast)在数据框中的数据。
#融合档位数和汽缸数这两个变量
mtcars.m <- melt(mtcars, id = c("mpg", "disp", "hp", "drat", "wt", "qsec", "vs", "carb")) 
head(mtcars)
head(mtcars.m)

映射(mapping)
(1) 映射的概念

    aes()函数是ggplot2中的映射函数, 所谓的映射即为数据集中的数据关联到相应的图形属性过程中一种对应关系, 例如:

> p1 <- ggplot(data = mtcars)
> summary(p1)
data: mpg, cyl, disp, hp, drat, wt, qsec, vs, am, gear, carb [32x11]
faceting: facet_null() 
> p2 <- ggplot(data = mtcars, mapping = aes(x = wt, y = hp, color = gear))
> summary(p2)
data: mpg, cyl, disp, hp, drat, wt, qsec, vs, am, gear, carb [32x11]
mapping:  x = wt, y = hp, colour = gear
faceting: facet_null()

可以发现, 在p2中, 通过aes()指定了横纵坐标分别为wt和hp, 颜色为gear这三种图形属性, 在ggplot2中不同的几何对象对应着不同的图形属性, 有关于几何对象的将在下面的小节讲解。
(2) 设定和映射
    映射是将一个变量中离散或连续的数据与一个图形属性中以不同的参数来相互关联, 而设定能够将这个变量中所有的数据统一为一个图形属性。

p <- ggplot(mtcars, aes(wt, mpg)) 
p + geom_point(color = "blue") #设定散点的颜色为蓝色
p + geom_point(aes( color = "blue"))

   

最后一行语句为错误的映射关系, 在aes中, color = “blue”的实际意思是把”blue”当为一个变量, 用这个变量里的数据去关联图形属性中的参数, 因为”blue”只含有一个字符变量, 默认情况下为离散变量, 按默认的颜色标度标记为桃红色

(3)分组(group)也是ggplot2种映射关系的一种, 默认情况下ggplot2把所有观测点分为了一组, 如果需要把观测点按额外的离散变量进行分组处理, 必须修改默认的分组设置。

p1 <- ggplot(data = mtcars, mapping = aes(x = wt, y = hp)) + geom_line() 
#默认分组设置, 即group=1
#geom_line为折线图的几何对象
p2 <- ggplot(data = mtcars, mapping = aes(x = wt, y = hp, group = factor(gear))) + geom_line() 
#把wt和hp所对应的观测点按gear(gear以因子化变为离散变量)进行分组

图层(layer)
    在上文中通过对数据和映射的讲解中, 我们已经采用过”+来添加图层, ggplot2中图层的概念和PS中图层的概念很像, 可以这样理解ggplot2中的图层:每个图层可以代表一个图形组件, 例如下面要介绍的几何对象、统计变换等图形组件, 这些组件以图层的方式叠加在一起构成一个绘图的整体, 在每个图层中的图形组件又可以分别设定数据、映射或其他相关参数, 因此组件之间又是具有相对独立性的。ggplot2中图层的设定是十分成功的, 因为这一过程是如此实用、方便而富有逻辑性。
(1) 在几何对象中设定映射
前面我们已在ggplot()中设定了映射了关系, 这种映射关系是默认的, 我们可以在后面的几何对象中沿用已设定的默认映射关系, 也可以随时在几何对象中进行更改。

p <- ggplot(mtcars, aes(x = mpg, y = wt, color = factor(gear)))
#设定默认的映射关系
p + geom_point()
#沿用默认的映射关系来绘制散点图
p + geom_point(aes(shape = factor(carb))) 
#添加图层中的shape的映射关系
p + geom_point(aes(y = carb))) 
#修改默认的y的映射关系, 注意图中y轴名称仍然以默认的wt表示
p + geom_point(aes(color = NULL))
 #删除默认的color映射关系


尽管上面三个有关例子在实际应用中很少去涉及, 但是很好的说明了图层、数据和映射之间的关系。
(2) 采用多个数据集或向量数据绘图
在很多种绘图场合中, 我们会运用到多个数据集或向量数据来进行图层叠加, 具体的例子如下

#构建不同于mtcars的数据集mtcars.c
mtcars.c <- transform(mtcars, mpg = mpg^2)
ggplot()+
  geom_point(aes(x = hp, y = mpg), data = mtcars, color = "red") + 
  geom_point(aes(x = mtcars$hp, y = mtcars$disp), color = "green")+ 
  #选用向量数据
  geom_point(aes(x = hp, y= mpg), data = mtcars.c, color = "blue") 
  #选用不同的数据集

几何对象(geom)和统计变换(stat)
    几何对象执行着图层的实际渲染, 控制着生成的图像类型。例如用geom_point()将会生成散点图, 而geom_line会生成折线图。统计变换即对数据进行统计变化, 通常以某种方式对数据信息进行汇总, 例如通过stat_smooth()添加光滑曲线。
每一个几何对象都有一个默认的统计变换, 并且每一个统计变换都有一个默认的几何对象。正因如此, 这一设定将会使绘图过程变的灵活多变。
在ggplot2的官方索引中, 已对ggplot2中所有的geom和stat组件进行了汇总, 更详细的内容, 可直接点开相应图形组件所对应的链接。下面对几个常用的几何对象和统计变换进行举例描述。
(1) geom_point()散点图

p <- ggplot(mtcars, aes(wt, mpg))
p + geom_point()
#更改颜色-连续变量
p + geom_point(aes(color = qsec))
#更改颜色-离散变量
p + geom_point(aes(color = factor(gear)))
#更改透明度
p + geom_point(aes(alpha = qsec))
#更改形状
p + geom_point(aes(shape = factor(gear)))
#更改点大小
p + geom_point(aes(size = qsec))
#两种颜色的叠加
p + geom_point(color = "grey50", size = 5) + geom_point(aes(color = qsec), size = 4)
#颜色和形状的叠加
p + geom_point(color = "grey50", size = 5) + geom_point(aes(shape = factor(gear)), size = 3)


(2) geom_histogram()

m <- ggplot(movies, aes(rating)) 
#这里使用movies数据集
m + geom_histogram()
m + geom_histogram(bin = 0.5)
#调整分箱(bin)数据
m + geom_histogram(bin = 1)
m + geom_histogram(bin = 2)


 

geom_histogram()这个几何对象默认使用stat_bin这个统计变换, 而这个统计变换会生成(1)count:每个组里观测值的数目, (2)density:每个组里观测值的密度和(3)x:组的中心位置这三个变量。生成的变量在ggplot()中的再使用..围起来, 因此可以用来生成如下的图

m + geom_histogram(bin = 0.5, aes(fill =..count..))
m + geom_histogram(bin = 0.5, aes(y = ..density..)) + geom_density()


元素位置的调整共有5种包括了(1)dodge:并排方式; (2)fill:堆叠图像元素, 并将高度标准化为1, (3)identity:不做任何调整; (4)jitter:给点增加扰动避免重合和(5)stack:堆叠图像元素。 

d <- ggplot(diamonds, aes(x = clarity, fill = cut ))
d + geom_histogram(position = "dodge")
d + geom_histogram(position = "fill")
d + geom_histogram(position = "stack")
ggplot(diamonds) + geom_point(aes(color, price/carat), position = "jitter")

    jitter使某每一个点在x轴的方向上产生随机的偏移, 从而减少了图形重叠的问题, 另一种介绍重叠的方式是改变点的透明度, 将在实战中的地图讨论。


很多情况下, 我们会采用固定的x轴和y轴值来进行作图, 此时需要用stat = “identity” 来申明, 即表示不对数据进行统计变换

A <- c(1, 2, 3, 4, 5, 6, 7, 8)
B <- c(2, 10, 11, 5, 6, 1, 10, 20)
ggplot() + geom_histogram(aes(x = A, y = B), stat = "identity")


(3) geom_smooth()
    geom_smooth()用来给数据添加平滑曲线, 所能采用的方法包括了lm, glm, gam, loess, rlm等, 这些方法需要通过加载公式来实现。

m <- ggplot(mtcars, aes(qsec, wt))
m + stat_smooth() + geom_point()
m + stat_smooth(se = FALSE) + geom_point()
 #取消默认的置信区间
m + stat_smooth(fill = "red", size = 2, alpha = 0.5, color = "green") + geom_point()
#更改置信区间和线条颜色
m + stat_smooth(method = "lm") + geom_point() 
#用一元一次线性方程拟合
m + stat_smooth(method = "lm", formula = y ~ poly(x, 3)) + geom_point() 
#使用一元二次方程拟合
require(splines)
require(MASS)
m + stat_smooth(method = "lm", formula = y ~ ns(x, 3)) + geom_point()
# 加载splines和MASS包, 使用自由度为3的自然样条来进行拟合
m <- ggplot(mtcars, aes(y = wt, x = mpg, group = factor(cyl)))
m + stat_smooth(method = lm, aes(color = factor(cyl), fill = factor(cyl))) + geom_point( aes(color = factor(cyl)))
#按cyl这个离散变量进行分组, 分别拟合数据


    得益于r包的丰富性, 我们可以采用极大似然或最小二乘法, 对多种现有的函数或自编函数来拟合曲线。例如, 在对剂量-效应曲线绘图的实战中, 采用drc包中的log-logistics四参数方程来拟合剂量-效应曲线。
    在这偏不长的博文中, 很难对ggplot2中所有几何对象和统计变换一一详尽, 更详细的内容可以在ggplot2的官方索引找到。但是通过前一小节有关于图层的讲解, 我们已能很容易的通过这些组件整合出一些复杂的图形, 而这些组件的原理也是相同的。

标度(scale)
    标度控制着数据到图形属性的映射, 更重要的一点是标度将我们的数据转化为视觉上可以感知的东西, 如大小、颜色、位置和形状。所以通过标度可以修改坐标轴和图例的参数。


表1所示, 所有标度构建器(scale constructor)都拥有一套通用的命名方案。它们以scale_开头, 接下来是图形属性的名称(例如,  color_、shape_或x_)最后以标度的名称结尾(例如gradient、hue或manual)。从表中可以发现, 标度是区分离散和连续变量的, 因此再对标度进行调整一定要注意区分。ggplot2中的标度可以粗略的分为4类:(1)位置标度:用于将连续型、离散型和日期-时间型变量映射到绘图区域, 以及构造对应的坐标轴; (2)颜色标度:用于将连续型和离散型变量映射到颜色; (3)手动离散型标度:用于将离散型变量映射到我们选择的符号大小、线条类型、形状或颜色, 以及创建对应的图例; 以及(4)同一型标度:用于直接将变量值绘制为图形属性, 而不去映射他们。
实际应用中修改标度最长用的有3个方面(1)修改图例和(2)修改图形属性和(3)修改坐标轴, 介于内容的复杂性, 建议详细的参考如下链接:
(1)索引中有关scale的内容:http://docs.ggplot2.org/current/index.html
(2)cookbook中有关图例的修改: 
http://www.cookbook-r.com/Graphs/Legends_(ggplot2)/
(3)cookbook中有关坐标轴的修改:
http://www.cookbook-r.com/Graphs/Axes_(ggplot2)/
(4)ColorBrewers配色方案:
http://colorbrewer.org
在后面的实例中, 每一张完美的图都需要对其标度进行细致的修改。

分面(facet)
即在一个页面上自动摆放多幅图形, 这一过程先将数据划分为多个子集, 然后将每个子集依次绘制到页面的不同面板中。ggplot2提供两种分面类型:网格型(facet_grid)封面型(facet_wrap)网格分面生成的是一个2维的面板网格, 面板的行与列通过变量来定义, 本质是2维的; 封装分面则先生成一个1维的面板条块, 然后再分装到2维中, 本质是1维的。
在很多情况下, 我们可能需要绘制有两个y轴的坐标系, 而在ggplot2中, 这种做法特别不提倡(stackover的讨论), 可解决的方法要么是把变量归一化, 要么便是采用分面方法

p <- ggplot(mtcars, aes(mpg, wt)) + geom_point()
p + facet_grid(. ~ cyl) #以cyl为分类变量
p + facet_wrap( ~ cyl, nrow = 3) #wrap与grid的区别
p + facet_grid(cyl ~ .) #以cyl为分类变量
p + facet_wrap( ~ cyl, ncol = 3) #wrap与grid的区别
p + facet_grid(vs ~ am) #以vs和am为分类变量
p + facet_wrap(vs ~ am, ncol = 2) #wrap与grid 的区别

p <- p + geom_smooth(method = "lm", se =F, aes(color = factor(cyl))) + geom_point(aes(color = factor(cyl)))
p + facet_grid(vs ~ am)
p + facet_grid(vs ~ am, margins = T) #使用margins来描述边际图
p + facet_grid( ~ cyl, scales = "free")
p + facet_grid( ~ cyl, scales = "free_x")

p <- ggplot(aes(cty, hwy), data = mpg) + geom_point()
p + facet_wrap( ~ cyl)
#调整scales的标度, 共有fixed, free, free_x和free_y四种变换
p + facet_wrap( ~ cyl, scales = "free") # 这里标度更改为free
p + facet_grid(. ~ cyl, scales = "free", space = "free") 
#space设置为free时, 每列的宽度与该列的标度范围成比例
##使用自由标度来替代双坐标轴的实战的一个例子中

主题(theme)
    主题系统控制着图形中的非数据元素外观, 它不会影响几何对象和标度等数据元素。主题修改是一个对绘图精雕细琢的过程, 主要对标题、坐标轴标签、图例标签文字调整, 以及网格线、背景、轴须的颜色搭配。

p <- ggplot(movies,  aes(x = rating)) + geom_histogram(bin = 1)
p + theme_bw() #白色背景
p + theme_grey() #默认浅灰色背景


    主题由控制图形外观的多个元素组成, 详见官方索引

##element_text()修改标签和标题
p <- p + labs(title = "histogram")
p + theme(plot.title = element_text(size = 20, color = "red", 
                                    hjust = 0, face = "bold", 
                                    angle = 180)) 


    内置元素共有四个基础类型:文本(text), 线条(line)、矩形(rectangle)、空白(blank), text与其他类型操作相类似, 具体的例子可参考索引, 此处用element_blank()来去除灰色背景。

p + theme(panel.background = element_blank()) #blank是去掉某种绘图元素

输出(ggsave)
    ggsave()是ggplot2种特有的输出函数, 是一种极为方便的出图方式

p <- ggplot(mtcars, aes(x = mpg, y = disp)) + geom_point()
ggsave( file = "mtcars_plot.png", width = 5, height = 6, type = "cairo", dpi = 600) 
#cairo为抗锯齿包, ggplot默认输出即为cairo处理

ggplot2支持eps矢量图输出, 其他可以支持的格式包括png, jpg, pdf等, 并通过ggsave可以方便的进行修改。

 

ggplot2作图实战

时间序列

数据下载

#用excel导入数据, 格式为csv
ori.data <- read.csv("lesson8.csv", header = F)
#以矩阵的方式读入数据, 按行排列, 每三列换一行
data <- matrix(as.matrix(ori.data), nrow(ori.data) / 3, 3, byrow = TRUE)
#关闭区域特定的时间编码方式
Sys.setlocale("LC_TIME", "C")
#用as.POSIXlt()读入字符串数据并转化为date数据, 赋值给date, 或as.Date()
date <- as.POSIXlt(data[, 1], tz = "", "%a %b %d %H:%M:%S HKT %Y")
#对ip和pv所在的列转化为数值型
IP <- as.numeric(data[, 2])
PV <- as.numeric(data[, 3])
head(data)
#恢复区域特地的时间编码方式
Sys.setlocale("LC_TIME", "")
#用ggplot2绘图
require(ggplot2)
#用reshape包中的melt函数分解数据
require(reshape2)
p.data <- data.frame(date, IP, PV)
meltdata <- melt(p.data, id = (c("date")))
#用对IP和PV做分页处理, y轴刻度自由变化
graphic <- ggplot(data = meltdata, aes(x = date, y = value, color = variable)) + geom_line() + geom_point()
graphic <- graphic + facet_grid(variable ~ ., scales = "free_y")
#美化, 添加标题, 坐标, 更改图例
graphic<- graphic + labs(x = "日期", y = "人次", title = "某网站7月至10月IP/PV统计") +
  theme(plot.title = element_text(size = 20, face = "bold")) +
  scale_colour_discrete(name = "",labels = c("IP","PV")) +
  theme(strip.text.y = element_text(angle = 0))

 

地图
 

require(maps)
require(ggplot2)
#用直方图看下pop整体的分布
#可以发现数据分布较变化较大, 所以对pop做log转化
qplot(pop, data = us.cities, binwidth = 0000, geom = "histogram")
qplot(log(pop), data = us.cities, binwidth = 0.03, geom = "histogram")

#绘制背景地图
USA.POP <- ggplot(us.cities, aes(x = long, y = lat)) + xlim(-130, -65) + borders("state", size=0.5)+
  geom_point(aes(size = log(pop), color = factor(capital), alpha = 1/50))+
  #对size标度的调整参考http://docs.ggplot2.org/0.9.3.1/scale_size.html
  scale_size(range=c(0, 7), name = "log(City population)")+
  #对离散型颜色变量的标度调整参考http://docs.ggplot2.org/0.9.3.1/scale_manual.html
  #对连续型颜色标量的标度调整参考http://docs.ggplot2.org/0.9.3.1/scale_brewer.html
  #和http://docs.ggplot2.org/0.9.3.1/scale_gradient2.html
  scale_color_manual(values = c("black", "red"), labels = c("state capital", "city"))+
  #调整图例
  guides(color = guide_legend(title=NULL)) + scale_alpha(guide = FALSE)+
  #绘制标题和坐标轴
  labs(x = "longtitude", y = "latitude", title = "City Population in the United States")+
  theme(plot.title = element_text(size=20))
  
#输出图像 并用cairo包进行抗锯齿处理
ggsave(USA.POP, file = "USA_POP.png", type = "cairo", width = 10, height = 6.75)

    当然, 这只是简单的地图绘制方法,统计之都上也有很多大牛来用R绘制各种各样精美的地图(1, 2)。


剂量-效应曲线

R中的drc包很容易对各种剂量-效应曲线进行绘图, 此处采用较为常用的log-logistic四参数方程拟合了剂量-效应曲线。

数据下载

ori.data <- read.csv("D-R curve.csv")
require(drc)
require(reshape2)
#把数据融合
melt.data <- melt(ori.data, id = c("dose"), value.name = "response")[, -2]
#用drc包中的log-logistic四参数方程进行拟合建模
model <- drm(response ~ dose, data = melt.data, fct = LL.4(names = c("Slope", "Lower Limit", "Upper Limit", "EC50")))
#确定x轴范围并构建数据集
min <- range(ori.data$dose)[1]
max <- range(ori.data$dose)[2]
line.data <- data.frame(d.predict = seq(min, max, length.out = 1000))
#用模型预测数据构建数据集
line.data$p.predict <- predict(model, newdata = line.data)
#构建绘图数据, 能够计算误差棒
require(plyr)
p.data <- ddply(melt.data, .(dose), colwise(mean))
p.data$sd <- ddply(melt.data, .(dose), colwise(sd))[,2]

require(ggplot2)
p <- ggplot() +
  geom_errorbar(data = p.data, width = 0.1, size = 1,
                aes(ymax = response + sd, ymin = response - sd, x = dose)) +
  geom_point(data = p.data, aes(x = dose, y = response), 
             color = "red", alpha = 0.5, size = 5) + 
  geom_line(data = line.data, aes(x = d.predict, y = p.predict), 
              size = 1, color = "blue") +
  #改变坐标轴间隔
  scale_x_log10(name = "Dose",
                breaks=c(0.05, 0.1, 0.5, 1, 5, 10, 50, 100)) +
  scale_y_continuous(name = "Response") + 
  theme_bw()
#查看拟合模型参数
summary(model)

邪恶之图

    绘图参数来自知乎, 此处亦有3D版本赠送

##用ggplot2来画函数
library(ggplot2)
#确定x轴区域
f <- ggplot(data.frame(x = c(0.00001, 1)), aes(x, color="pink", size=2))
test <- function(x) {(1/36)*exp(-((36*x-36/2.71828182845905)^4))-3*x*log10(x)}
f <- f + stat_function(fun = test)+theme(legend.position="none") + 
  #旋转坐标轴
  coord_flip()
f

注:封面图片来自facebook社交数据可视化

分享到:
  1. Pingback: ggplot2学习文章推荐 | 希言堂