R 绘图系统 grid🎈

前言

大家都知道,R 存在两个主要的绘图系统,basegrid (ggplot2,lattice)
目前R 主要用作统计软件,绘图工具,数据框处理优秀软件。
如果大部分进行文本处理,首选python,感觉R不算一个很严谨的编程语言.

R 需要学好绘图功能,特别是ggplot2-- 基于grid 进行编写。
了解了grid,拼图的过程心里明白好多.
想更加了解ggplot 是什么,首选应该看一下Y 叔 meme 包 . 效果如下:

2020年2月4日23:47:18

grid包 主要内容🚲

viewport : 建立视图,及其多个视图的关系
These functions create viewports, which describe rectangular regions on a graphics device and define a number of coordinate systems within those regions.

  • viewport
  • vpList(...)
  • vpStack(...)
  • vpTree(parent, children)

grob :画图内容及其不同内容的关系:

  • grob() and gTree() are the basic creators, grobTree() and gList() take several grobs to build a new one.

小试牛刀😑:用grid来简单模仿上图 参数解析

I.?textGrob

textGrob(label, x = unit(0.5, "npc"), y = unit(0.5, "npc"), just = "centre", hjust = NULL, vjust = NULL, rot = 0, check.overlap = FALSE, default.units = "npc", name = NULL, gp = gpar(), vp = NULL)
前面一些参数,设置内容;gp 设置主题,vp 设置图图层.

II.?gpar 查看具体theme 参数,比如颜色
Valid parameter names are:

col  Colour for lines and borders.
fill     Colour for filling rectangles, polygons, ...
alpha    Alpha channel for transparency
lty  Line type
lwd  Line width
lex  Multiplier applied to line width
lineend  Line end style (round, butt, square)
linejoin     Line join style (round, mitre, bevel)
linemitre    Line mitre limit (number greater than 1)
fontsize     The size of text (in points)
cex  Multiplier applied to fontsize
fontfamily   The font family
fontface     The font face (bold, italic, ...)
lineheight   The height of a line as a multiple of the size of text
font     Font face (alias for fontface; for backward compatibility)
III.画图

textGrobrsaterGrob 将文字和图片转换成grob 类型,用grid.draw 画图.

###
# library(magick)
#library(grid)
# 文字
#underlineText <- function(text, gpar, vjust, hjust){ 
#  txt <- textGrob(text, gp=gpar, vjust = vjust, hjust = hjust) 
#  undLine <- segmentsGrob(x0 = txt$x - grobWidth(txt), y0 = txt$y - unit(0.55, "grobheight", txt), x1 = txt$x, y1 = txt$y - unit(0.55, #"grobheight", txt)) 
#  gt <- grobTree(txt, undLine) 
#} 

#A <- underlineText("My Sample Header", gpar(fontfamily="serif",fontsize=16, fontface="bold",lineheight=1), vjust = 0, hjust = 2)



### ------------------------
library(magick)
library(grid)
gp1 <- gpar(col = "white", lty = "solid", 
     fontsize=16, cex = 2,
     lwd = 3,fontfamily = "Impact")

# 文字2
A1 <- textGrob("CODE", 
         x = unit(0.5, "npc"), 
         y = unit(0.8, "npc"),
         just = "centre",gp=gp1)

A2 <- textGrob("ALL THE THINGS!", 
               x = unit(0.5, "npc"), 
               y = unit(0.2, "npc"),
               just = "centre",gp=gp1)

# 图片
u <- system.file("angry8.jpg",package = "meme")
x <- image_read(u, density="density")
B <- rasterGrob(x)

###
grid.draw(gList(B,A1,A2))

2020年2月5日00:38:22

由此可以知道,meme 使用类似方法进行完成.
接下来学习一点grid 知识






Grid 包学习参考资料📕

陈同公众号R画图

CSDN - R语言grid包使用笔记——viewport
徐洲更简书-「R绘图」grid学习笔记之grid.layout
R统计绘图(2):grid布局

  • viewport
  • viewport树
  • viewport行列布局

*** CSDN - R语言绘图底层系统之Grid包

  • 此文着重介绍viewport()函数
  • 还有一个重要参数layout,可支持行列布局。任意摆放图形

*** R实战:grid包


Grid 实战📝

grid包是一个底层的绘图系统,能够灵活地控制图形输出的外观和布局,但是grid包不提供创建完整图形的高级绘图系统,例如,ggplot2lattice,而是提供绘制开发这些高级绘图的基础接口,例如:定制lattice的输出,产生高水平图或非统计图,为输出添加复杂注释。在绘图时,有时候会遇到这样一种情景,客户想把多个代表不同KPI的图形分布到同一个画布(Page)上,而且每一个图形都是单独绘制的。对于这种需求,可以使用grid包来实现,grid包能把图形逐个地添加到画布中,并按照业务的需求,把图形摆放到合适的位置上去。在布局完成之后,把图形绘制出来。

一,grid包概述

grid包不仅可以输出图形,还可以产生可编辑的图形组件,这些图形组件可以被复用和重组,并能把图形输出到指定的位置上。在使用grid包之前,需要安装和载入grid包:

install.packages("grid")
library(grid)

Tips:
grid.rect(...) 等价于 rect <- rectGrob(...) grid.draw(rect)
长度单位是npc绘制的图像不是绝对的大小

1,绘图原语 : 画图内容

常用的绘图原语的明明格式是grid.**,用于输出图形:

grid.rect(...)   
grid.lines(...)  
grid.polygon(...)
grid.circle(...)
grid.text(...)

用户可以通过参数修改绘图函数输出的图形:

grid.rect(x = unit(0, “native"), y = unit(1.5, “npc"), height = unit(0.5, "inches"), width = unit(0.8, “lines”),
    gp=gpar(col="red", lwd=2, fill="yellow", lty="dotted"))
image.png
2,Grob对象 : 画图内容

每一个绘图原语都对应一个Grob,grob的命名格式是**Grob,Grob对象是一个可编辑的图形组件,该组件保留图形的所有属性,但不会立即输出图形:

rectGrob(...)
linesGrob(...)
polygonGrob(...)
circleGrob(...)
textGrob(..)

要输出Grob表示的图形,可以使用grid.draw()函数绘制图形:

rect <- rectGrob(...) 
grid.draw(rect)

## 举例
rect <- rectGrob(x = unit(0, "native"), 
         y = unit(1.5, "npc"), 
         height = unit(0.5, "inches"), 
         width = unit(0.8, "lines"),
         gp=gpar(col="red", lwd=2, 
                 fill="yellow", lty="dotted"))
grid.draw(rect)

image.png






二,核心对象viewport : 规定画图的区域

绘制图形需要画布,是R的绘图设备,在grid包中,画布被定义为page,通过函数newpage()创建一个新的画布:

grid.newpage()

viewport是grid包的核心对象,简单来说,它就是画布中的一个矩形的绘图区域,直译为视口,通过viewport()函数新建一个viewport对象:

viewport(x = unit(0.5, "npc"), y = unit(0.5, "npc"),
         width = unit(1, "npc"), height = unit(1, "npc"),
         default.units = "npc", just = "centre",
         gp = gpar(), clip = "inherit",
         xscale = c(0, 1), yscale = c(0, 1),
         angle = 0,
         layout = NULL, 
         layout.pos.row = NULL, layout.pos.col = NULL,
         name = NULL)

参数注释:

  • x:视口的几何中心点相对页面左下角原点的x坐标轴,默认单位是npc
  • y:视口的几何中心点相对页面左下角原点的y坐标轴,默认单位是npc
  • width:视口的宽度(x轴方向)
  • height:视口的高度(y轴方向)
  • default.units:默认单位为npc (Normalised Parent Coordinates),含义是规范化化的父区域坐标
  • just:x和y所指的位置,默认为矩形中心位置
  • gp:gpar对象,用于设置图形参数;
  • clip:裁剪区域,有效值是“on”,“inherit”或“off”,指示剪裁到视口范围内,从父视口继承剪裁区域,或者完全关闭剪裁。 为了后向兼容性,逻辑值TRUE对应于“on”,而FALSE对应于“inherit”
  • xscale,yscale:两个数值元素的向量,用于表示坐标轴的最小值和最大值。
  • angle:把视口逆时针旋转的角度
  • layout:布局(grid.layout)对象,用于把视口划分为多个子区域
  • layout.pos.row,layout.pos.col:子区域在父布局中的行位置和列位置
  • name:此视口的名字,用于搜索和定位

viewport是绘图的基础,创建一个viewport:

vp <- viewport(x = 0.5, y = 0.5, width = 0.5, height = 0.25, angle=45)

通过函数grid.show.viewport()查看创建的视口:

grid.show.viewport(viewport(x=0.6, y=0.6, width=unit(1, "inches"), height=unit(1, "inches"), angle=30))

height和width是矩形的长和宽,x和y是视口中心点(也就是,矩形的几何中心点)距离x坐标抽和y坐标轴的距离:

image






三,基于viewport绘制图形

TIPS:
大体意思: 规定画图区域,在在此区域画图

使用grid包绘图时,首先要创建一个空的画布:

grid.newpage()

在画布中创建viewport对象:

vp <- viewport(x = 0.5, y = 0.5, width = 0.5, height = 0.25, angle=45)

此时,画布中是空的,需要把viewport推到画布中:

pushViewport(vp)

viewport是绘图的区域,也就是说,基于viewport绘制图形,在视口规定的范围内作图,例如,向视口中绘制矩形。

grid.rect()

image






四,viewport树: 添加不同视图的逻辑关系

grid包为每一个画布维护了一个由viewport构成的树,树的根节点是由系统创建的,名字是ROOT的viewport,每一个节点都是一个viewport。活跃viewport是树的当前位置,在树中是唯一的,用户只能向活跃viewport中绘图,所有的操作都是基于活跃viewport。viewport()函数用于创建viewport,而一个viewport只有被push到viewport树中,才能在其区域中绘图。

通过5个函数实现对viewport树的遍历和更新:

  • pushViewport()函数:向活跃viewport中添加一个viewport,作为树中的活跃viewport,原活跃viewport变成父viewport,这意味着,当一个viewport被push到树中时,该viewport变成活跃viewport,是原活跃viewport的子viewport。
  • popViewport()函数:把活跃viewport从树中删除,其父viewport变成活跃viewport。
  • upViewport()函数:导航到活跃viewport的父viewport,当前viewport变成活跃viewport,原viewport不会被删除;
  • downViewport()函数:导航到活跃viewport的父viewport,当前viewport变成活跃viewport,原viewport不会被删除;
  • searchViewport()函数:根据viewport的名字,导航到任意viewport,当前viewport变成活跃viewport,原viewport不会被删除。

注意:当向树中push一个viewport时,如果树中存在一个级别(level)相同,名字相同的viewport,那么push操作会把该viewport替换掉。

There is only ever one current viewport, which is the current position within the viewport tree. All drawing and viewport operations are relative to the current viewport. When a viewport is pushed it becomes the current viewport. When a viewport is popped, the parent viewport becomes the current viewport. Use upViewport to navigate to the parent of the current viewport, without removing the current viewport from the viewport tree. Use downViewport to navigate to a viewport further down the viewport tree and seekViewport to navigate to a viewport anywhere else in the tree.

查看当前的viewport树结构:

current.vpTree()

例如,下面我们连续push三个viewport到一个图形中。

grid.newpage()
colvec <- c('red', 'green', 'blue')
xvec <- c(0.3, 0.4, 0.5)
for (i in 1 : 3) {
  vp <- viewport(x = xvec[i], y = 0.5, width = 0.4, height = 0.4,
                 gp = gpar(col = colvec[i]))
  pushViewport(vp)
  grid.rect()
}

绘制的图形依次嵌套,这说明,每push一次,原活跃viewport都变成父节点,把当前的veiwport作为子viewport:

image
Tips: 如果想让三个视图并排,添加upViewport() ,每次以ROOT 视图来画矩形。
grid.newpage()
colvec <- c('red', 'green', 'blue')
xvec <- c(0.3, 0.4, 0.5)
for (i in 1 : 3) {
  vp <- viewport(x = xvec[i], y = 0.5, width = 0.4, height = 0.4,
                 gp = gpar(col = colvec[i]))
  pushViewport(vp)
  grid.rect()
  upViewport()
}

image.png






五,布局 : grid.layout

TIPS:
通过grid 语法有两个方法进行拼图布局。(现在已经有很多函数/包可以完成 ggarrange,plot_grid(),patchwork,cowplot等等)

  • grid.layout布局,再提取子视图,并取名为margin1 ,2,...,再通过vpTree()建立vp树,再通过seekViewport("margin1") ,选中当前的视图,再进行依次作图grid.text

  • 创建多个图形, 创建布局,分割视口,并push当前视口 , 把图形输出到布局的不同区域中.



grid包中定义了布局对象,布局是矩形的子分区,也就是说,布局(layout)把一个矩形区域细分为更小的分区。

grid.layout(nrow = 1, ncol = 1,
    widths = unit(rep_len(1, ncol), "null"), heights = unit(rep_len(1, nrow), "null"),
    default.units = "null", respect = FALSE, just="centre")

参数注释:

  • nrow,ncol:布局分为多少个行和列,每一个行和列构成的单元叫做分区(subdivision)
  • widths,heights:每一个分区的宽和高
  • default.units:默认单位
  • respect:逻辑值,如果为true,指定行高度和列宽度都遵守。
  • just:指定对齐方式,有效的值是:"left", "right", "centre", "center", "bottom", 和 "top".

1,创建布局

把top.vp视口分割为3X3的分区,使用函数grid.show.layout()查看布局,创建的布局如下图所示:

layout <- grid.layout(nrow=3, ncol=3,
                   widths=unit(c(5, 1, 2), c("lines", "null", "lines")),
                   heights=unit(c(5, 1, 4), c("lines", "null", "lines")))

top.vp <-viewport(layout=layout, name="top")

grid.show.layout(layout)
image

创建一系列的viewport,占用布局的各个分区,由于没有push任何viewport,因此画布中没有绘制任何图形。在为每个视口命名时,使用统一的格式:margin+数值,如下代码所示:

margin1 <- viewport(layout.pos.col = 2, layout.pos.row = 3, name = "margin1") #(3,2)
margin2 <- viewport(layout.pos.col = 1, layout.pos.row = 2, name = "margin2") #(2,1)
margin3 <- viewport(layout.pos.col = 2, layout.pos.row = 1, name = "margin3") #(1,2)
margin4 <- viewport(layout.pos.col = 3, layout.pos.row = 2, name = "margin4") #(2,3)
plot <- viewport(layout.pos.col = 2, layout.pos.row = 2, name = "plot")       #(2,2)

R用数字来表示位置,数值代表的含义是:1=Buttom,2=Left,3=Top,4=Right,视口被布局分割的分区如下图所示:

image

2,创建viewport树

使用vpList()把视口排列成一个树形结构,并把top.vp作为视图的父节点,把所有其他视口作为子节点,使用vpTree()创建一个viewport树:

splot <- vpTree(top.vp, vpList(margin1, margin2, margin3, margin4, plot))

把整个viewport树push到活跃视口中,这样,在绘图区域中,我们可以在不同的散点视口中绘制图形。

 pushViewport(splot)

在把整个树push到活跃视口之后,就可以在不同的区域内绘制图形,使用seekViewport()函数按照视口名称切换到指定的视口,并把当前视口激活。

seekViewport("plot")
grid.xaxis()
grid.yaxis()
grid.rect()
grid.points(x, y)

完整的代码如下:

library(grid)

layout <- grid.layout(nrow=3, ncol=3,
                      widths=unit(c(5, 1, 2), c("lines", "null", "lines")),
                      heights=unit(c(5, 1, 4), c("lines", "null", "lines")))
#grid.show.layout(layout)

top.vp <-viewport(layout=layout,name="top")
#grid.show.viewport(top.vp)

x <- runif(10)
y <- runif(10)
xscale <- extendrange(x)
yscale <- extendrange(y)

margin1 <- viewport(layout.pos.col = 2, layout.pos.row = 3, name = "margin1")
margin2 <- viewport(layout.pos.col = 1, layout.pos.row = 2, name = "margin2")
margin3 <- viewport(layout.pos.col = 2, layout.pos.row = 1, name = "margin3")
margin4 <- viewport(layout.pos.col = 3, layout.pos.row = 2, name = "margin4")
plot <- viewport(layout.pos.col = 2, layout.pos.row = 2, name = "plot",xscale = xscale, yscale = yscale)

splot <- vpTree(top.vp, vpList(margin1, margin2, margin3, margin4, plot))
#grid.show.viewport(splot)

pushViewport(splot)

seekViewport("plot")
grid.xaxis()
grid.yaxis()
grid.rect()
grid.points(x, y,pch=20)

seekViewport("margin1")
grid.text("Random X", y = unit(1, "lines"))

seekViewport("margin2")
grid.text("Random Y", x = unit(1, "lines"), rot = 90)

3,把图形逐个打印到视口中

利用布局在同一个画布中绘制多个图形的另外一种方法是使用print()函数,代码摘抄于《R统计绘图(2):grid布局》。

step1,创建多个图形

library(grid)
library(ggplot2)
# prepare ggplot charts
p.hist.len <- ggplot(iris) + geom_histogram(aes(x=Sepal.Length))
p.hist.wid <- ggplot(iris) + geom_histogram(aes(x=Sepal.Width)) + coord_flip()
p.scatter <- ggplot(iris) + geom_point(aes(x=Sepal.Length, y=Sepal.Width))

step2,创建布局,分割视口,并push当前视口

grid.newpage()
pushViewport(viewport(layout = grid.layout(3, 3)))

step3,把图形输出到布局的不同区域中

print(p.scatter, vp=viewport(layout.pos.row=2:3, layout.pos.col=1:2))
print(p.hist.len, vp=viewport(layout.pos.row=1, layout.pos.col=1:2))
print(p.hist.wid, vp=viewport(layout.pos.row=2:3, layout.pos.col=3))

2020年2月5日15:56:50






六,unit对象和gpar对象

在grid包中,unit对象用于表示:长度和单位,gpar对象用于设置:图形参数

1,unit对象
用unit对象表示长度的大小和单位:

unit(x, units, data=NULL)
参数注释:

x:数值向量
units:单位向量
2,gpar对象

用gapr对象表示图形参数:

col: Colour for lines and borders.
fill: Colour for filling rectangles, polygons, ...
alpha: Alpha channel for transparency
lty: Line type
lwd: Line width
lex: Multiplier applied to line width
lineend: Line end style (round, butt, square)
linejoin: Line join style (round, mitre, bevel)
linemitre: Line mitre limit (number greater than 1)
fontsize: The size of text (in points)
cex: Multiplier applied to fontsize
fontfamily: The font family
fontface: The font face (bold, italic, ...)
lineheight: The height of a line as a multiple of the size of text
font: Font face (alias for fontface; for backward compatibility)






运用grid其他例子

1.ggimage 例子

ggimage 封装geom_image 函数很方便,下面用grid 来尝试.

image.png

用grid 简单实现如下:

# 加载包
library(grid)
library(magick)

# 建立新画布
grid.newpage()
set.seed(666)
colvec <- sample(c('red', 'green', 'blue'),10,replace = T)
# 视图位置
xvec <- c(2, 2, 2, 2, 2, 3, 3, 3.5, 3.5, 4)/6
yvec <- c(2, 3, 4, 5, 6, 4, 6, 3, 5, 2)/8

## R 图标grob类型
img <- system.file("img", "Rlogo.png", package = "png")
x <- image_read(img, density="density")
B <- rasterGrob(x)


## 每个视图添R 图标

for (i in 1 : 10) {
  vp <- viewport(x = xvec[i],
                 y = yvec[i], 
                 width = 0.1, height = 0.1)# ,
                 # gp = gpar(col = colvec[i],fill="grey30"))
  pushViewport(vp)
  grid.draw(B)
  upViewport()
}
2020年2月5日16:49:43

存在差异,加入ggplot 主题背景,尝试如下:

# 加载包
library(grid)
library(magick)

# 建立新画布
grid.newpage()
set.seed(666)
colvec <- sample(c('red', 'green', 'blue'),10,replace = T)
# 视图位置
xvec <- c(2, 2, 2, 2, 2, 3, 3, 3.5, 3.5, 4)/6
yvec <- c(2, 3, 4, 5, 6, 4, 6, 3, 5, 2)/8
d <- data.frame(x = xvec*6, y = yvec*8)

## R 图标grob类型
img <- system.file("img", "Rlogo.png", package = "png")
x <- image_read(img, density="density")
B <- rasterGrob(x)

## 1.添加ggplot 背景
G <- ggplot(d, aes(x, y))+
  labs(title = "学习grob包",
       subtitle="2020年2月5日17:10:19",
       caption="@caokai001")
  
grid.draw(G)

## 2.每个视图添R 图标
for (i in 1 : 10) {
  vp <- viewport(x = xvec[i],
                 y = yvec[i], 
                 width = 0.1, height = 0.1)# ,
                 # gp = gpar(col = colvec[i],fill="grey30"))
  pushViewport(vp)
  grid.draw(B)
  upViewport()
}

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341