Cursor 交互 ggplot:字体 API 与 httpgd 视口

font_info(weight=) 与 UseMethod(depth) on NULL:现象、根因与修复

Published

October 28, 2026

经验来自《中国旱区农业技术发展报告2026》在 Cursor 终端里手工 print() 分面图。书稿 knitr 出图不受这两处交互故障影响。可拷贝源:../R/httpgd-warmup.R;版本要求见下文字体栈表。

在 Cursor 集成 R 终端里执行 ggplot(...) 或 print(p),可能先后遇到两类报错。它们看起来像「这张图的几何图层写错了」,实际都发生在打开默认绘图设备或第一帧 grid 视口,与 geom_col / facet_grid 无关。隔离方法:同一对象用 ggplot2::ggsave(..., device = ragg::agg_png);若 ragg 能出图、交互 print() 不能,就不要改分析脚本。

发生情形

Cursor 用户设置 "r.plot.useHttpgd": true,会话监视器把 options(device) 换成:

function(...) {
  httpgd::hgd(silent = TRUE)
  .vsc$request("httpgd", url = httpgd::hgd_url())
}

print.ggplot → grid.newpage() 会走这条设备。knitr 走 dev = "ragg_png"(../R/knitr-ragg.R),不走 httpgd。所以 Quarto html/docx 图正常、终端预览失败,是预期分裂,不是章脚本坏了。

mtcars 散点图若同样报错,即可排除具体业务图。

故障一:font_info(..., weight=)

Error in font_info(font, weight = if (bold) "bold" else "normal", italic = italic) :
  unused argument (weight = if (bold) "bold" else "normal")

典型栈:print.ggplot → grid.newpage → httpgd::hgd → unigd::ugd → match_family → font_info(..., weight=)。设备在给 sans / serif / mono 做别名时就失败,names(dev.cur()) 常为 "null device"。

根因:unigd 的 match_family() 已按 systemfonts ≥ 1.3 的 API 传 weight=;项目若锁 systemfonts 1.2.x,font_info() 形参仍是 bold。httpgd / unigd 常由编辑器另装进项目库,不在 lockfile 里,与 knitr 用的 ragg 不是一条链。find.package("svglite") 为空不能否定这条路径:调用方是 unigd。

修复:升字体栈到互相匹配的版本,只钉这几条,不要全量 renv::snapshot():

包 须达到 作用
systemfonts ≥ 1.3.0(已验证 1.3.2) font_info() 接受 weight=
textshaping 与之配套(已验证 1.0.5) ragg / unigd 的 shaping
ragg 与之配套(已验证 1.5.2) knitr ragg_png 的 C API
renv::install(c("systemfonts@1.3.2", "textshaping@1.0.5", "ragg@1.5.2"))
# 然后只改 renv.lock 对应 Version

装完必须退出占用 DLL 的 R 会话再开新会话。验收:

names(formals(systemfonts::font_info))  # 须含 weight
unigd:::match_family("sans")            # 应返回族名,如 Arial

故障二:UseMethod("depth") 作用在 NULL

Error in UseMethod("depth") :
  no applicable method for 'depth' applied to an object of class "NULL"

根因:grid + 交互设备(httpgd / unigd)的已知问题。监视器在 hgd() 之后立刻 .vsc$request() 拉 SVG,第一帧 ggplot 绘制时 viewport 路径变成 NULL,postDraw 里 depth(path) 没有 depth.NULL 方法。ragg ggsave 通常不受影响。

修复:项目 .Rprofile(放在 renv/activate.R 之后)source httpgd-warmup.R:

if (interactive() && file.exists("scripts/book-plot/R/httpgd-warmup.R")) {
  source("scripts/book-plot/R/httpgd-warmup.R")
}

它做两件事:

  1. registerS3method("depth", "NULL", function(x, ...) 0L, envir = asNamespace("grid")),路径为 NULL 时不 pop 视口。
  2. 一次性 addTaskCallback:等 options(device) 已是 httpgd 包装函数后预开 httpgd::hgd(),避免「开设备 + 第一张 ggplot」叠在同一帧。选项名 bookplot.httpgd_warmed / bookplot.httpgd_tries。

若项目跑 languageserver 等后台会话,在外层再加自己的环境变量判断,避免后台进程也注册回调。现有会话可临时执行第 1 条。

诊断顺序

traceback()
names(dev.cur())
getOption("device")
names(formals(systemfonts::font_info))
packageVersion("systemfonts")
packageVersion("ragg")
find.package("httpgd", quiet = TRUE)
ggplot(mtcars, aes(wt, mpg)) + geom_point()
tf <- tempfile(fileext = ".png")
ggplot2::ggsave(tf, last_plot(), device = ragg::agg_png, width = 11, height = 6)
file.exists(tf)
观察 结论
ggsave(ragg) 成功、print() 失败 不要改分析脚本;查 device / 字体栈
traceback 含 match_family / weight= systemfonts 低于 1.3,或与 unigd 代差
traceback 含 depth(path) / postDraw httpgd 首帧视口;用 httpgd-warmup.R
knitr / Quarto 图空白 另一件事:Windows png() + simsun;用 ragg_png,再跑 check-fig-png.py

不要做

  • 为交互预览报错去改 geom_* / facet_grid。
  • showtext_auto() 与 ragg_png 叠用。
  • 只升 systemfonts、留下旧 textshaping / ragg DLL。
  • 升包后不重启仍占用 library 的 R 会话。
  • 把 httpgd 首帧 depth 误判成「必须关掉 r.plot.useHttpgd」。