Skip to content

Commit 6b2bfd0

Browse files
zorroozclaude
andcommitted
feat: add mark_path, mark_polygon, mark_smooth, mark_hex, mark_density_2d, mark_corr
- mark_path: wraps geom_path (G2 Path, trajectories) - mark_polygon: wraps geom_polygon (G2 Polygon) - mark_smooth: wraps geom_smooth (VL layer+transform, G2 transform) - mark_hex: wraps geom_hex (G2 Heatmap, 2D binning) - mark_density_2d: wraps geom_density_2d with filled option (G2 Density contour) - mark_corr: correlation matrix heatmap with hclust reorder (G2 Cell) - All registered as unsupported on plotit_composite - Tests: 18 new tests covering basic, params, variants - 378 pass, 0 fail Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6f4fec7 commit 6b2bfd0

10 files changed

Lines changed: 726 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ export(label_title)
1818
export(mark_area)
1919
export(mark_bar)
2020
export(mark_boxplot)
21+
export(mark_corr)
2122
export(mark_density)
23+
export(mark_density_2d)
24+
export(mark_hex)
2225
export(mark_histogram)
2326
export(mark_line)
2427
export(mark_map)
28+
export(mark_path)
2529
export(mark_point)
30+
export(mark_polygon)
2631
export(mark_rect)
2732
export(mark_rule)
33+
export(mark_smooth)
2834
export(mark_text)
2935
export(mark_violin)
3036
export(plotit)

R/mark.R

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,285 @@ S7::method(mark_rule, plotit_class) <- function(
498498
)
499499
}
500500

501+
# ---- mark_path ----
502+
#' Path layer
503+
#'
504+
#' Adds a path layer connecting observations in their original order.
505+
#' Use for trajectories, time-ordered connected points, or custom
506+
#' drawing orders.
507+
#'
508+
#' @param plot A plotit object
509+
#' @param mapping Optional new aesthetics
510+
#' @param data Optional data for this layer
511+
#' @param position Position adjustment.
512+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
513+
#' @param rasterize_dpi DPI for rasterization (default 300).
514+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
515+
#' @param ... Other arguments passed to `geom_path`
516+
#' @return Modified plotit object
517+
#' @references
518+
#' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/path}{Path}
519+
#' @examples
520+
#' df <- data.frame(x = 1:10, y = cumsum(runif(10, -1, 1)))
521+
#' plotit(df, encode(x = x, y = y)) |> mark_path()
522+
#' @export
523+
mark_path <- S7::new_generic(
524+
"mark_path", "plot",
525+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
526+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
527+
S7::S7_dispatch()
528+
}
529+
)
530+
._register_mark_method(mark_path, ggplot2::geom_path)
531+
532+
# ---- mark_polygon ----
533+
#' Polygon layer
534+
#'
535+
#' Adds a filled polygon layer. Each group forms one polygon;
536+
#' subgroups are separated by `NA` rows or the `group` aesthetic.
537+
#'
538+
#' @param plot A plotit object
539+
#' @param mapping Optional new aesthetics
540+
#' @param data Optional data for this layer
541+
#' @param position Position adjustment.
542+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
543+
#' @param rasterize_dpi DPI for rasterization (default 300).
544+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
545+
#' @param ... Other arguments passed to `geom_polygon`
546+
#' @return Modified plotit object
547+
#' @references
548+
#' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/polygon}{Polygon}
549+
#' @examples
550+
#' tri <- data.frame(x = c(0, 1, 0.5), y = c(0, 0, 1))
551+
#' plotit(tri, encode(x = x, y = y)) |> mark_polygon(fill = "skyblue")
552+
#' @export
553+
mark_polygon <- S7::new_generic(
554+
"mark_polygon", "plot",
555+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
556+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
557+
S7::S7_dispatch()
558+
}
559+
)
560+
._register_mark_method(mark_polygon, ggplot2::geom_polygon)
561+
562+
# ---- mark_smooth ----
563+
#' Smoothed conditional mean layer
564+
#'
565+
#' Adds a smoothed conditional mean line with a confidence band.
566+
#' Aids the eye in seeing patterns in the presence of overplotting.
567+
#'
568+
#' @param plot A plotit object
569+
#' @param mapping Optional new aesthetics
570+
#' @param data Optional data for this layer
571+
#' @param position Position adjustment.
572+
#' @param method Smoothing method: `"auto"` (loess for n<1000, gam otherwise),
573+
#' `"lm"`, `"glm"`, `"gam"`, or `"loess"`.
574+
#' @param formula Formula to use in the smoothing function.
575+
#' @param se If `TRUE` (default), display confidence interval around smooth.
576+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
577+
#' @param rasterize_dpi DPI for rasterization (default 300).
578+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
579+
#' @param ... Other arguments passed to `geom_smooth`
580+
#' @return Modified plotit object
581+
#' @references
582+
#' Vega-Lite: achieved via \code{layer(point) + layer(line) + transform(regression)}
583+
#'
584+
#' AntV G2: achieved via transform pipeline
585+
#' @examples
586+
#' plotit(mtcars, encode(x = wt, y = mpg)) |> mark_smooth()
587+
#' @export
588+
mark_smooth <- S7::new_generic(
589+
"mark_smooth", "plot",
590+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
591+
method = NULL, formula = NULL, se = NULL,
592+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
593+
S7::S7_dispatch()
594+
}
595+
)
596+
597+
#' @export
598+
S7::method(mark_smooth, plotit_class) <- function(
599+
plot, mapping = NULL, data = NULL, position = NULL, ...,
600+
method = NULL, formula = NULL, se = NULL,
601+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
602+
params <- rlang::list2(...)
603+
params$method <- method
604+
params$formula <- formula
605+
params$se <- se
606+
do.call(function(...) {
607+
._mark_impl(plot, mapping, data, position, ggplot2::geom_smooth,
608+
rasterize, rasterize_dpi, rasterize_dev, ...)
609+
}, params)
610+
}
611+
612+
# ---- mark_hex ----
613+
#' Hexagonal heatmap layer
614+
#'
615+
#' Divides the x-y plane into hexagonal bins and fills each by the
616+
#' count (or other aggregation) of observations in that bin.
617+
#' Ideal for visualizing overplotting in large datasets.
618+
#'
619+
#' @param plot A plotit object
620+
#' @param mapping Optional new aesthetics
621+
#' @param data Optional data for this layer
622+
#' @param position Position adjustment.
623+
#' @param bins Number of bins along both axes (default 30).
624+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
625+
#' @param rasterize_dpi DPI for rasterization (default 300).
626+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
627+
#' @param ... Other arguments passed to `geom_hex`
628+
#' @return Modified plotit object
629+
#' @references
630+
#' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/heatmap}{Heatmap} (corelib)
631+
#' @examples
632+
#' plotit(diamonds[sample(nrow(diamonds), 1000), ],
633+
#' encode(x = carat, y = price)) |> mark_hex(bins = 20)
634+
#' @export
635+
mark_hex <- S7::new_generic(
636+
"mark_hex", "plot",
637+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
638+
bins = NULL,
639+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
640+
S7::S7_dispatch()
641+
}
642+
)
643+
644+
#' @export
645+
S7::method(mark_hex, plotit_class) <- function(
646+
plot, mapping = NULL, data = NULL, position = NULL, ...,
647+
bins = NULL,
648+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
649+
params <- rlang::list2(...)
650+
params$bins <- bins
651+
do.call(function(...) {
652+
._mark_impl(plot, mapping, data, position, ggplot2::geom_hex,
653+
rasterize, rasterize_dpi, rasterize_dev, ...)
654+
}, params)
655+
}
656+
657+
# ---- mark_density_2d ----
658+
#' 2D density contour layer
659+
#'
660+
#' Adds 2D kernel density estimate contours. Use `filled = TRUE`
661+
#' for filled density bands via [ggplot2::geom_density_2d_filled].
662+
#'
663+
#' @param plot A plotit object
664+
#' @param mapping Optional new aesthetics
665+
#' @param data Optional data for this layer
666+
#' @param position Position adjustment.
667+
#' @param filled If `TRUE`, use filled density contours.
668+
#' @param bins Number of contour bins (for filled mode).
669+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
670+
#' @param rasterize_dpi DPI for rasterization (default 300).
671+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
672+
#' @param ... Other arguments passed to the underlying geom
673+
#' @return Modified plotit object
674+
#' @references
675+
#' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/density}{Density} (corelib, contour mode)
676+
#' @examples
677+
#' plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
678+
#' mark_density_2d()
679+
#' @export
680+
mark_density_2d <- S7::new_generic(
681+
"mark_density_2d", "plot",
682+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
683+
filled = FALSE, bins = NULL,
684+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
685+
S7::S7_dispatch()
686+
}
687+
)
688+
689+
#' @export
690+
S7::method(mark_density_2d, plotit_class) <- function(
691+
plot, mapping = NULL, data = NULL, position = NULL, ...,
692+
filled = FALSE, bins = NULL,
693+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
694+
geom_fun <- if (filled) ggplot2::geom_density_2d_filled else ggplot2::geom_density_2d
695+
dots <- rlang::list2(...)
696+
if (!is.null(bins)) dots$bins <- bins
697+
# Only clear default_color when the layer provides colour/fill
698+
if (!is.null(mapping) && (!is.null(mapping$colour) || !is.null(mapping$fill))) {
699+
plot <- ._clear_default_color(plot, mapping)
700+
}
701+
pos <- position
702+
if (is.null(pos) && !is.null(plot@meta@dodge) && plot@meta@dodge > 0) {
703+
pos <- ggplot2::position_dodge(plot@meta@dodge)
704+
}
705+
geom <- if (is.null(pos)) {
706+
do.call(geom_fun, c(list(mapping = mapping, data = data), dots))
707+
} else {
708+
do.call(geom_fun, c(list(mapping = mapping, data = data, position = pos), dots))
709+
}
710+
.add_geom(plot, geom,
711+
rasterize = rasterize, rasterize_dpi = rasterize_dpi,
712+
rasterize_dev = rasterize_dev
713+
)
714+
}
715+
716+
# ---- mark_corr ----
717+
#' Correlation matrix heatmap
718+
#'
719+
#' Computes a correlation matrix from numeric data columns, optionally
720+
#' reorders by hierarchical clustering, and renders it as a tile heatmap.
721+
#'
722+
#' @param plot A plotit object. Numeric columns are extracted from the
723+
#' plot data for correlation computation.
724+
#' @param method Correlation method: `"pearson"` (default), `"spearman"`,
725+
#' or `"kendall"`.
726+
#' @param reorder If `TRUE` (default), reorder rows and columns by
727+
#' hierarchical clustering.
728+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
729+
#' @param rasterize_dpi DPI for rasterization (default 300).
730+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
731+
#' @param ... Other arguments passed to `geom_tile`
732+
#' @return Modified plotit object
733+
#' @references
734+
#' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/cell}{Cell} (correlation matrix expression)
735+
#' @examples
736+
#' plotit(mtcars, encode()) |> mark_corr()
737+
#' @export
738+
mark_corr <- S7::new_generic(
739+
"mark_corr", "plot",
740+
function(plot, method = c("pearson", "spearman", "kendall"),
741+
reorder = TRUE, ...,
742+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
743+
S7::S7_dispatch()
744+
}
745+
)
746+
747+
#' @export
748+
S7::method(mark_corr, plotit_class) <- function(
749+
plot, method = c("pearson", "spearman", "kendall"),
750+
reorder = TRUE, ...,
751+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
752+
method <- match.arg(method)
753+
# Extract numeric columns from plot data
754+
raw_data <- plot@gg$data
755+
num_cols <- vapply(raw_data, is.numeric, logical(1))
756+
if (sum(num_cols) < 2) {
757+
cli::cli_abort("{.fn mark_corr} requires at least 2 numeric columns.")
758+
}
759+
mat <- stats::cor(raw_data[, num_cols, drop = FALSE], method = method)
760+
# Hierarchical clustering reorder
761+
if (reorder) {
762+
ord <- stats::hclust(stats::as.dist(1 - abs(mat)))$order
763+
mat <- mat[ord, ord]
764+
}
765+
# Melt to long form
766+
df <- expand.grid(
767+
Var1 = factor(rownames(mat), levels = rownames(mat)),
768+
Var2 = factor(colnames(mat), levels = colnames(mat))
769+
)
770+
df$value <- as.vector(mat)
771+
# Build tile
772+
mapping <- encode(x = Var1, y = Var2, fill = value)
773+
geom <- ggplot2::geom_tile(mapping = mapping, data = df, ...)
774+
.add_geom(plot, geom,
775+
rasterize = rasterize, rasterize_dpi = rasterize_dpi,
776+
rasterize_dev = rasterize_dev
777+
)
778+
}
779+
501780
# ---- mark_bar (hand-written: geom_col vs geom_bar dispatch) ----
502781
#' Bar layer
503782
#'

R/zzz.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ for (.generic_name in c(
6969
"mark_point", "mark_line", "mark_bar", "mark_boxplot",
7070
"mark_histogram", "mark_density", "mark_area", "mark_text",
7171
"mark_violin", "mark_map", "mark_rect", "mark_rule",
72+
"mark_path", "mark_polygon", "mark_smooth", "mark_hex",
73+
"mark_density_2d", "mark_corr",
7274
"scale_color", "scale_fill", "scale_size", "scale_alpha",
7375
"scale_shape", "scale_linetype", "scale_x", "scale_y",
7476
"project_cartesian", "project_polar", "project_parallel",

man/mark_corr.Rd

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

0 commit comments

Comments
 (0)