メモ:ggplot2::geom_sf()でポリゴンの境界に点線を引きたいときはsf::st_union()とsf::st_line_merge()

geom_sf()と戯れていて、ちょっと点線でも引いてみるか、と思ってやってみると、なにこれ??という感じの汚い線の図になりました。

library(ggplot2)
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, proj.4 4.9.3

nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

ggplot(nc) +
  geom_sf(
    colour   = "black", 
    fill     = "lightgreen",
    linetype = "dotted",
    size     = 1.5
  )

さすがにもうちょっとなんとかなるのでは?と思って光の速さでRStudio Communityに相談しつついろいろ調べたときのメモ。

ポリゴンの面と線は別々に描く

ncはポリゴンなので、面と線は1つのレイヤーで描くことができます。 なんですけど、描画は「面を全地物に対して描いてから線」という順番ではなく、「面を描いて線」というのを地物ごとにやっていく感じになるみたいです。 なので、面と線でレイヤーを分けるとある程度きれいに描画されます。

library(ggplot2)
library(patchwork)

nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

p1 <- ggplot(nc) +
  geom_sf(linetype = "dotted", size = 1.5) +
  theme_minimal()

p2 <- ggplot(nc) +
  # 面だけを描くので線は透明に
  geom_sf(colour = "transparent") +
  # 線だけを描くので面は透明に
  geom_sf(fill = "transparent", linetype = "dotted", size = 1.5) +
  theme_minimal()

p1 / p2

ちゃんと境界線をつくる

ポリゴン間にある境界線は、当然ポリゴンの数だけ重ね描きされるので、点線だと点がずれて描かれることになります。 なので、ポリゴンの輪郭で満足せず、ちゃんと境界線をつくってからプロットする必要があるっぽい。

library(sf)

# わかりやすいように、地物を2つだけ取り出し、色分けのためにIDを割り振る
nc_12 <- tibble::rownames_to_column(nc[1:2,], "id")

nc_12_merged <- nc_12 %>%
  # 境界線を取り出す
  st_boundary() %>%
  # 1つの地物にまとめる(このときにsfからsfcになる)
  st_union() %>%
  # 重なっている線をマージする
  st_line_merge() %>%
  # ggplot2でプロットするためにsfcをsfにする
  st_sf()

p1 <- ggplot(nc_12) +
  geom_sf(aes(colour = id), fill = "transparent", linetype = "dotted", size = 4) +
  theme_minimal()

p2 <-  ggplot(nc_12_merged) +
  geom_sf(colour = "black", fill = "transparent", linetype = "dotted", size = 4) +
  theme_minimal()

p1 / p2

そもそも線の幅と図形の細かさに比べてドットの間隔が広すぎる

dottedというのは線と空白の長さの比が1:3になるような線なんですが(けっこう深い部分に定義されている)、もうちょっと狭い間隔じゃないときれいにならないみたいです。というのをRStudio Communityの質問についた回答で知りました。 線種の指定の仕方の詳細は省きますが(詳しくは以下を参照)、1:1の間隔にしたい場合は"11"を指定します。

まとめ

これをふまえたプロットと初めのプロットを比較すると、こんな感じになるはずです。

p1 <- ggplot(nc) +
  geom_sf(
    colour   = "black", 
    fill     = "lightgreen",
    linetype = "dotted",
    size     = 1.5
  ) +
  theme_minimal()

nc_merged <- nc %>%
  st_boundary() %>%
  st_union() %>%
  st_line_merge() %>%
  st_sf()

p2 <- ggplot() +
  geom_sf(data = nc, colour = "transparent", fill = "lightgreen") +
  geom_sf(data = nc_merged, colour = "black",  linetype = "11", size = 1.5) +
  theme_minimal()

p1 / p2

うーん、やっぱそもそも線が太すぎるのかも...。まあ初めよりはだいぶましになったということで。

追記(2018/06/06)

st_boundary()の部分はst_cast("MULTILINESTRING")でもいいらしい。なるほど!