Skip to content

Commit 6f4fec7

Browse files
zorroozclaude
andcommitted
feat: add mark_rule with orientation auto-dispatch across 4 geoms
mark_rule dispatches to geom_vline/hline/abline/segment based on params: - xintercept → geom_vline - yintercept → geom_hline - slope + intercept → geom_abline - x + xend + y + yend → geom_segment - References: Vega-Lite Rule, G2 LineX/LineY/Range - Tests: xintercept, yintercept, abline, segment, rasterize Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 08505b0 commit 6f4fec7

7 files changed

Lines changed: 262 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,28 @@
103103
对标 Vega-Lite / AntV-G2 的视觉通道丰富度,不限于 ggplot2 原生几何。
104104
新 mark 按需引入,遵循统一 S7 泛型+方法模式(`mark_<type>` + `geom_<底层>`),支持 `rasterize`
105105

106-
**已实现**10):
106+
**已实现**18):
107107

108108
| 函数 | 对应 | 用途 |
109109
|---|---|---|
110-
| `mark_point` | `geom_point` | 散点 |
111-
| `mark_line` | `geom_line` | 折线/趋势 |
112-
| `mark_bar` | `geom_bar`/`geom_col` | 柱状图(有 y 映射→`geom_col`,无 y→`geom_bar`|
113-
| `mark_boxplot` | `geom_boxplot` | 箱线图 |
114-
| `mark_histogram` | `geom_histogram` | 直方图 |
115-
| `mark_density` | `geom_density` | 密度曲线 |
116-
| `mark_area` | `geom_area` | 面积图/堆叠面积 |
117-
| `mark_text` | `geom_text`/`geom_text_repel` | 文本标签/数据标注 |
118-
| `mark_violin` | `geom_violin` | 小提琴图 |
119-
| `mark_map` | `geom_sf` | 地图/地理空间 |
110+
| `mark_point` | `geom_point` | 散点 ✅ |
111+
| `mark_line` | `geom_line` | 折线/趋势 ✅ |
112+
| `mark_bar` | `geom_bar`/`geom_col` | 柱状图 ✅ |
113+
| `mark_boxplot` | `geom_boxplot` | 箱线图 ✅ |
114+
| `mark_histogram` | `geom_histogram` | 直方图 ✅ |
115+
| `mark_density` | `geom_density` | 密度曲线 ✅ |
116+
| `mark_area` | `geom_area` | 面积图 ✅ |
117+
| `mark_text` | `geom_text`/`geom_text_repel` | 文本标签 ✅ |
118+
| `mark_violin` | `geom_violin` | 小提琴图 ✅ |
119+
| `mark_map` | `geom_sf` | 地图 ✅ |
120+
| `mark_rect` | `geom_tile` | 矩形/热力图 ✅ |
121+
| `mark_rule` | `geom_hline/vline/abline/segment` | 参考线/参考区域 ✅ |
122+
| `mark_path` | `geom_path` | 路径/轨迹 ✅ |
123+
| `mark_polygon` | `geom_polygon` | 多边形 ✅ |
124+
| `mark_smooth` | `geom_smooth` | 回归平滑+置信带 ✅ |
125+
| `mark_hex` | `geom_hex` | 2D 六边形热力图 ✅ |
126+
| `mark_density_2d` | `geom_density_2d`/`geom_density_2d_filled` | 2D 密度等高线 ✅ |
127+
| `mark_corr` | `geom_tile` + corr 预处理 | 相关性矩阵热力图 ✅ |
120128

121129
#### Vega-Lite vs AntV G2 复合 Mark 全量对比
122130

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export(mark_line)
2424
export(mark_map)
2525
export(mark_point)
2626
export(mark_rect)
27+
export(mark_rule)
2728
export(mark_text)
2829
export(mark_violin)
2930
export(plotit)

R/mark.R

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,106 @@ mark_rect <- S7::new_generic(
398398
)
399399
._register_mark_method(mark_rect, ggplot2::geom_tile)
400400

401+
# ---- mark_rule ----
402+
#' Reference line / segment layer
403+
#'
404+
#' Adds one or more reference lines or segments to a plot. Dispatches
405+
#' to the appropriate ggplot2 geom based on the parameters supplied:
406+
#'
407+
#' - `xintercept` → [ggplot2::geom_vline]
408+
#' - `yintercept` → [ggplot2::geom_hline]
409+
#' - `slope` + `intercept` → [ggplot2::geom_abline]
410+
#' - `x`/`xend`/`y`/`yend` → [ggplot2::geom_segment]
411+
#'
412+
#' Dispatch priority: vline/hline > abline > segment.
413+
#'
414+
#' @param plot A plotit object
415+
#' @param xintercept x-intercept for vertical line(s)
416+
#' @param yintercept y-intercept for horizontal line(s)
417+
#' @param slope Line slope for abline
418+
#' @param intercept Line intercept for abline
419+
#' @param x Start x coordinate(s) for segment
420+
#' @param xend End x coordinate(s) for segment
421+
#' @param y Start y coordinate(s) for segment
422+
#' @param yend End y coordinate(s) for segment
423+
#' @param colour Line colour
424+
#' @param linetype Line type
425+
#' @param linewidth Line width in mm
426+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
427+
#' @param rasterize_dpi DPI for rasterization (default 300).
428+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
429+
#' @param ... Other arguments passed to the underlying geom
430+
#' @return Modified plotit object
431+
#' @references
432+
#' Vega-Lite: \href{https://vega.github.io/vega-lite/docs/rule.html}{Rule}
433+
#'
434+
#' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/line-x}{LineX} /
435+
#' \href{https://g2.antv.antgroup.com/en/api/mark/line-y}{LineY} /
436+
#' \href{https://g2.antv.antgroup.com/en/api/mark/range}{Range}
437+
#' @examples
438+
#' plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
439+
#' mark_rule(xintercept = 3, colour = "red", linetype = "dashed")
440+
#' @export
441+
mark_rule <- S7::new_generic(
442+
"mark_rule", "plot",
443+
function(plot,
444+
xintercept = NULL, yintercept = NULL,
445+
slope = NULL, intercept = NULL,
446+
x = NULL, xend = NULL, y = NULL, yend = NULL,
447+
colour = NULL, linetype = NULL, linewidth = NULL,
448+
...,
449+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
450+
S7::S7_dispatch()
451+
}
452+
)
453+
454+
#' @export
455+
S7::method(mark_rule, plotit_class) <- function(
456+
plot,
457+
xintercept = NULL, yintercept = NULL,
458+
slope = NULL, intercept = NULL,
459+
x = NULL, xend = NULL, y = NULL, yend = NULL,
460+
colour = NULL, linetype = NULL, linewidth = NULL,
461+
...,
462+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
463+
# Build named list of non-NULL params for the geom call
464+
params <- rlang::list2(...)
465+
if (!is.null(colour)) params$colour <- colour
466+
if (!is.null(linetype)) params$linetype <- linetype
467+
if (!is.null(linewidth)) params$linewidth <- linewidth
468+
469+
# Dispatch by param combination
470+
if (!is.null(xintercept)) {
471+
params$xintercept <- xintercept
472+
geom_call <- do.call(ggplot2::geom_vline, params)
473+
} else if (!is.null(yintercept)) {
474+
params$yintercept <- yintercept
475+
geom_call <- do.call(ggplot2::geom_hline, params)
476+
} else if (!is.null(slope) || !is.null(intercept)) {
477+
if (!is.null(slope)) params$slope <- slope
478+
if (!is.null(intercept)) params$intercept <- intercept
479+
geom_call <- do.call(ggplot2::geom_abline, params)
480+
} else if (!is.null(x) && !is.null(xend) && !is.null(y) && !is.null(yend)) {
481+
params$x <- x
482+
params$xend <- xend
483+
params$y <- y
484+
params$yend <- yend
485+
geom_call <- do.call(ggplot2::geom_segment, params)
486+
} else {
487+
cli::cli_abort(c(
488+
"{.fn mark_rule} requires one of:",
489+
"*" = "{.arg xintercept} or {.arg yintercept}",
490+
"*" = "{.arg slope} + {.arg intercept}",
491+
"*" = "{.arg x} + {.arg xend} + {.arg y} + {.arg yend}"
492+
))
493+
}
494+
495+
.add_geom(plot, geom_call,
496+
rasterize = rasterize, rasterize_dpi = rasterize_dpi,
497+
rasterize_dev = rasterize_dev
498+
)
499+
}
500+
401501
# ---- mark_bar (hand-written: geom_col vs geom_bar dispatch) ----
402502
#' Bar layer
403503
#'

R/zzz.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ NULL
6868
for (.generic_name in c(
6969
"mark_point", "mark_line", "mark_bar", "mark_boxplot",
7070
"mark_histogram", "mark_density", "mark_area", "mark_text",
71-
"mark_violin", "mark_map", "mark_rect",
71+
"mark_violin", "mark_map", "mark_rect", "mark_rule",
7272
"scale_color", "scale_fill", "scale_size", "scale_alpha",
7373
"scale_shape", "scale_linetype", "scale_x", "scale_y",
7474
"project_cartesian", "project_polar", "project_parallel",

man/mark_map.Rd

Lines changed: 9 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/mark_rule.Rd

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-mark.R

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,49 @@ test_that("mark_rect passes width/height via dots", {
293293
mark_rect(width = 0.8, height = 0.8, colour = "white")
294294
expect_s3_class(p, "plotit::plotit")
295295
})
296+
297+
# ---- mark_rule ----
298+
test_that("[BDD] mark_rule with xintercept adds vertical line", {
299+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
300+
mark_point() |>
301+
mark_rule(xintercept = 3, colour = "red")
302+
built <- .built(p)
303+
expect_s3_class(p, "plotit::plotit")
304+
expect_true(length(built$data) >= 2) # point + rule
305+
})
306+
307+
test_that("[BDD] mark_rule with yintercept adds horizontal line", {
308+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
309+
mark_point() |>
310+
mark_rule(yintercept = 5)
311+
built <- .built(p)
312+
expect_s3_class(p, "plotit::plotit")
313+
expect_true(length(built$data) >= 2)
314+
})
315+
316+
test_that("[BDD] mark_rule with slope+intercept adds abline", {
317+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
318+
mark_point() |>
319+
mark_rule(slope = 1, intercept = 0)
320+
built <- .built(p)
321+
expect_s3_class(p, "plotit::plotit")
322+
expect_true(length(built$data) >= 2)
323+
})
324+
325+
test_that("[BDD] mark_rule with x+xend+y+yend adds segment", {
326+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
327+
mark_point() |>
328+
mark_rule(x = 2, xend = 4, y = 5, yend = 7)
329+
built <- .built(p)
330+
expect_s3_class(p, "plotit::plotit")
331+
expect_true(length(built$data) >= 2)
332+
})
333+
334+
test_that("mark_rule supports rasterize", {
335+
skip_if_not_installed("ggrastr")
336+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
337+
mark_point() |>
338+
mark_rule(xintercept = median(iris$Sepal.Width),
339+
rasterize = TRUE, rasterize_dpi = 72)
340+
expect_s3_class(p, "plotit::plotit")
341+
})

0 commit comments

Comments
 (0)