@@ -779,6 +779,322 @@ S7::method(mark_corr, plotit_class) <- function(
779779 )
780780}
781781
782+ # ---- mark_errorbar ----
783+ # ' Error bar layer
784+ # '
785+ # ' Adds error bars showing confidence intervals, standard errors,
786+ # ' or other variability measures. Data should include columns for
787+ # ' `ymin`/`ymax` (vertical) or `xmin`/`xmax` (horizontal).
788+ # '
789+ # ' @param plot A plotit object
790+ # ' @param mapping Optional new aesthetics (must include `ymin`/`ymax`
791+ # ' or `xmin`/`xmax`)
792+ # ' @param data Optional data for this layer
793+ # ' @param position Position adjustment.
794+ # ' @param width Width of the error bar caps (default 0.5).
795+ # ' @param orientation `"vertical"` (default) or `"horizontal"`.
796+ # ' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
797+ # ' @param rasterize_dpi DPI for rasterization (default 300).
798+ # ' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
799+ # ' @param ... Other arguments passed to the underlying geom
800+ # ' @return Modified plotit object
801+ # ' @references
802+ # ' Vega-Lite: \href{https://vega.github.io/vega-lite/docs/errorbar.html}{Errorbar} (composite mark)
803+ # ' @examples
804+ # ' df <- data.frame(
805+ # ' x = c("A", "B"), y = c(10, 20), ymin = c(8, 18), ymax = c(12, 22))
806+ # ' plotit(df, encode(x = x, y = y, ymin = ymin, ymax = ymax)) |>
807+ # ' mark_errorbar(width = 0.3)
808+ # ' @export
809+ mark_errorbar <- S7 :: new_generic(
810+ " mark_errorbar" , " plot" ,
811+ function (plot , mapping = NULL , data = NULL , position = NULL , ... ,
812+ width = 0.5 , orientation = c(" vertical" , " horizontal" ),
813+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
814+ S7 :: S7_dispatch()
815+ }
816+ )
817+
818+ # ' @export
819+ S7 :: method(mark_errorbar , plotit_class ) <- function (
820+ plot , mapping = NULL , data = NULL , position = NULL , ... ,
821+ width = 0.5 , orientation = c(" vertical" , " horizontal" ),
822+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
823+ orientation <- match.arg(orientation )
824+ geom_fun <- if (orientation == " horizontal" ) {
825+ ggplot2 :: geom_errorbarh
826+ } else {
827+ ggplot2 :: geom_errorbar
828+ }
829+ params <- rlang :: list2(... )
830+ params $ width <- width
831+ do.call(function (... ) {
832+ ._mark_impl(plot , mapping , data , position , geom_fun ,
833+ rasterize , rasterize_dpi , rasterize_dev , ... )
834+ }, params )
835+ }
836+
837+ # ---- mark_significance ----
838+ # ' Significance annotation layer
839+ # '
840+ # ' Adds statistical significance brackets and labels between groups.
841+ # ' This is a **syntax-sugar composite mark** that combines
842+ # ' `mark_rule` and `mark_text` internally.
843+ # '
844+ # ' Equivalent expansion:
845+ # ' \preformatted{
846+ # ' p |> mark_rule(x = comp$group1, xend = comp$group2,
847+ # ' y = comp$y_position, yend = comp$y_position) |>
848+ # ' mark_text(x = midpoint, y = comp$y_position + y_offset,
849+ # ' label = comp$label)
850+ # ' }
851+ # '
852+ # ' @param plot A plotit object
853+ # ' @param comparisons A data frame with columns: `group1`, `group2`,
854+ # ' `label`, and optionally `y_position`. Character columns are
855+ # ' matched against the x-axis variable.
856+ # ' @param y_position Numeric vector of y-positions for the brackets.
857+ # ' If omitted, auto-computed from data range.
858+ # ' @param y_offset Text offset above the bracket line (default 0.5).
859+ # ' In data units.
860+ # ' @param line_colour Colour for the bracket lines (default `"grey30"`).
861+ # ' @param line_width Width of bracket lines (default 0.3).
862+ # ' @param text_size Size of significance label text (default 3.5).
863+ # ' @param tip_length Length of bracket end-tick lines (default 0.02
864+ # ' as fraction of x-axis range).
865+ # ' @param ... Additional arguments passed to `mark_text()`
866+ # ' @return Modified plotit object
867+ # ' @examples
868+ # ' df <- data.frame(group = c("A", "B", "C"), value = c(5, 8, 4))
869+ # ' comp <- data.frame(
870+ # ' group1 = c("A", "A"), group2 = c("B", "C"),
871+ # ' label = c("**", "ns"))
872+ # ' plotit(df, encode(x = group, y = value)) |>
873+ # ' mark_bar() |>
874+ # ' mark_significance(comp, y_position = c(9, 6))
875+ # ' @export
876+ mark_significance <- S7 :: new_generic(
877+ " mark_significance" , " plot" ,
878+ function (plot , comparisons , y_position = NULL , y_offset = NULL ,
879+ line_colour = " grey30" , line_width = 0.3 ,
880+ text_size = 3.5 , tip_length = 0.02 , ... ) {
881+ S7 :: S7_dispatch()
882+ }
883+ )
884+
885+ # ' @export
886+ S7 :: method(mark_significance , plotit_class ) <- function (
887+ plot , comparisons , y_position = NULL , y_offset = NULL ,
888+ line_colour = " grey30" , line_width = 0.3 ,
889+ text_size = 3.5 , tip_length = 0.02 , ... ) {
890+ if (! is.data.frame(comparisons )) {
891+ cli :: cli_abort(" {.arg comparisons} must be a data frame." )
892+ }
893+ required <- c(" group1" , " group2" , " label" )
894+ missing_cols <- setdiff(required , names(comparisons ))
895+ if (length(missing_cols ) > 0 ) {
896+ cli :: cli_abort(
897+ " {.arg comparisons} must have columns: {.val {required}}."
898+ )
899+ }
900+ # Extract data range for auto y_position
901+ d <- plot @ gg $ data
902+ y_var <- rlang :: eval_tidy(plot @ gg $ mapping $ y , d )
903+ y_range <- range(y_var , na.rm = TRUE )
904+ y_span <- diff(y_range )
905+ if (is.null(y_offset )) y_offset <- y_span * 0.02
906+ # Auto-compute y_position if not provided
907+ if (is.null(y_position )) {
908+ y_position <- y_range [2 ] + y_span * 0.1 + seq_len(nrow(comparisons )) * y_span * 0.08
909+ }
910+ # Get x positions (handle factor/character group1/group2)
911+ x_var <- rlang :: eval_tidy(plot @ gg $ mapping $ x , d )
912+ x_levels <- if (is.factor(x_var )) levels(x_var ) else sort(unique(as.character(x_var )))
913+ x_positions <- seq_along(x_levels )
914+ names(x_positions ) <- x_levels
915+ # Draw brackets
916+ for (i in seq_len(nrow(comparisons ))) {
917+ g1 <- as.character(comparisons $ group1 [i ])
918+ g2 <- as.character(comparisons $ group2 [i ])
919+ x1 <- if (g1 %in% names(x_positions )) x_positions [[g1 ]] else as.numeric(g1 )
920+ x2 <- if (g2 %in% names(x_positions )) x_positions [[g2 ]] else as.numeric(g2 )
921+ if (is.na(x1 ) || is.na(x2 )) next
922+ y_pos <- if (i < = length(y_position )) y_position [i ] else y_position [1 ] + (i - 1 ) * y_span * 0.08
923+ # Bracket line
924+ geome <- ggplot2 :: geom_segment(
925+ mapping = encode(x = x1 , xend = x2 , y = y_pos , yend = y_pos ),
926+ colour = line_colour , linewidth = line_width
927+ )
928+ plot <- .add_geom(plot , geome )
929+ # Left tick
930+ tick_len <- if (is.factor(x_var )) tip_length * length(x_levels ) else tip_length * diff(range(x_positions ))
931+ geome <- ggplot2 :: geom_segment(
932+ mapping = encode(
933+ x = x1 , xend = x1 , y = y_pos - tick_len , yend = y_pos
934+ ),
935+ colour = line_colour , linewidth = line_width
936+ )
937+ plot <- .add_geom(plot , geome )
938+ # Right tick
939+ geome <- ggplot2 :: geom_segment(
940+ mapping = encode(
941+ x = x2 , xend = x2 , y = y_pos - tick_len , yend = y_pos
942+ ),
943+ colour = line_colour , linewidth = line_width
944+ )
945+ plot <- .add_geom(plot , geome )
946+ # Label
947+ geome <- ggplot2 :: geom_text(
948+ mapping = encode(
949+ x = (x1 + x2 ) / 2 , y = y_pos + y_offset , label = comparisons $ label [i ]
950+ ),
951+ size = text_size , ...
952+ )
953+ plot <- .add_geom(plot , geome )
954+ }
955+ plot
956+ }
957+
958+ # ---- mark_lollipop ----
959+ # ' Lollipop chart layer
960+ # '
961+ # ' Creates a lollipop chart: a point anchored by a stem to a reference
962+ # ' line. This is a **syntax-sugar composite mark** combining
963+ # ' `geom_segment` and `geom_point`.
964+ # '
965+ # ' Equivalent expansion:
966+ # ' \preformatted{
967+ # ' p |> mark_rule(x = x, xend = x, y = ref, yend = y) |>
968+ # ' mark_point(x = x, y = y)
969+ # ' }
970+ # '
971+ # ' @param plot A plotit object
972+ # ' @param mapping Optional new aesthetics
973+ # ' @param data Optional data for this layer
974+ # ' @param stem_colour Colour for the stem lines (default `"grey50"`).
975+ # ' @param stem_width Line width for stems (default 0.5).
976+ # ' @param point_size Point size for the lollipop head (default 3).
977+ # ' @param ... Other arguments passed to `mark_point()`
978+ # ' @return Modified plotit object
979+ # ' @examples
980+ # ' df <- data.frame(cat = LETTERS[1:5], val = c(3, 7, 2, 9, 5))
981+ # ' plotit(df, encode(x = cat, y = val)) |>
982+ # ' mark_lollipop(point_size = 4, stem_colour = "grey70")
983+ # ' @export
984+ mark_lollipop <- S7 :: new_generic(
985+ " mark_lollipop" , " plot" ,
986+ function (plot , mapping = NULL , data = NULL ,
987+ stem_colour = " grey50" , stem_width = 0.5 ,
988+ point_size = 3 , ... ) {
989+ S7 :: S7_dispatch()
990+ }
991+ )
992+
993+ # ' @export
994+ S7 :: method(mark_lollipop , plotit_class ) <- function (
995+ plot , mapping = NULL , data = NULL ,
996+ stem_colour = " grey50" , stem_width = 0.5 ,
997+ point_size = 3 , ... ) {
998+ d <- data %|| % plot @ gg $ data
999+ m <- mapping %|| % plot @ gg $ mapping
1000+ # Extract x and y from mapping
1001+ x_col <- rlang :: eval_tidy(m $ x , d )
1002+ y_col <- rlang :: eval_tidy(m $ y , d )
1003+ # Stem: segment from 0 to y
1004+ stem_mapping <- encode(x = x_col , xend = x_col , y = 0 , yend = y_col )
1005+ geome <- ggplot2 :: geom_segment(
1006+ mapping = stem_mapping ,
1007+ colour = stem_colour , linewidth = stem_width
1008+ )
1009+ plot <- .add_geom(plot , geome )
1010+ # Point at the top
1011+ point_mapping <- encode(x = x_col , y = y_col )
1012+ # Determine fill from mapping or default
1013+ fill_val <- if (! is.null(m $ fill )) {
1014+ rlang :: eval_tidy(m $ fill , d )
1015+ } else {
1016+ NULL
1017+ }
1018+ plot <- plot | > mark_point(
1019+ mapping = m , data = d , size = point_size , ...
1020+ )
1021+ plot
1022+ }
1023+
1024+ # ---- mark_dumbbell ----
1025+ # ' Dumbbell comparison chart layer
1026+ # '
1027+ # ' Creates a dumbbell chart with two connected points showing before/after
1028+ # ' or paired comparisons. This is a **syntax-sugar composite mark**
1029+ # ' combining two `mark_point` calls and a `geom_segment`.
1030+ # '
1031+ # ' Equivalent expansion:
1032+ # ' \preformatted{
1033+ # ' p |> mark_rule(x = x, xend = x, y = y_start, yend = y_end) |>
1034+ # ' mark_point(x = x, y = y_start, colour = colour_start) |>
1035+ # ' mark_point(x = x, y = y_end, colour = colour_end)
1036+ # ' }
1037+ # '
1038+ # ' @param plot A plotit object
1039+ # ' @param mapping Optional new aesthetics
1040+ # ' @param data Optional data for this layer
1041+ # ' @param colour_start Colour for the start point (default `"#4E79A7"`).
1042+ # ' @param colour_end Colour for the end point (default `"#E15759"`).
1043+ # ' @param line_colour Colour for the connecting line (default `"grey50"`).
1044+ # ' @param point_size Size for both dumbbell points (default 3).
1045+ # ' @param line_width Width for the connecting line (default 1).
1046+ # ' @param ... Other arguments passed to `mark_point()` calls
1047+ # ' @return Modified plotit object
1048+ # ' @examples
1049+ # ' df <- data.frame(cat = LETTERS[1:5], before = c(3, 5, 2, 8, 4),
1050+ # ' after = c(7, 6, 5, 10, 6))
1051+ # ' plotit(df, encode(x = cat, y = before, yend = after)) |>
1052+ # ' mark_dumbbell()
1053+ # ' @export
1054+ mark_dumbbell <- S7 :: new_generic(
1055+ " mark_dumbbell" , " plot" ,
1056+ function (plot , mapping = NULL , data = NULL ,
1057+ colour_start = " #4E79A7" , colour_end = " #E15759" ,
1058+ line_colour = " grey50" , point_size = 3 ,
1059+ line_width = 1 , ... ) {
1060+ S7 :: S7_dispatch()
1061+ }
1062+ )
1063+
1064+ # ' @export
1065+ S7 :: method(mark_dumbbell , plotit_class ) <- function (
1066+ plot , mapping = NULL , data = NULL ,
1067+ colour_start = " #4E79A7" , colour_end = " #E15759" ,
1068+ line_colour = " grey50" , point_size = 3 ,
1069+ line_width = 1 , ... ) {
1070+ d <- data %|| % plot @ gg $ data
1071+ m <- mapping %|| % plot @ gg $ mapping
1072+ x_col <- rlang :: eval_tidy(m $ x , d )
1073+ y_col <- rlang :: eval_tidy(m $ y , d )
1074+ yend_col <- rlang :: eval_tidy(m $ yend , d )
1075+ # Connecting line
1076+ segment_mapping <- encode(
1077+ x = x_col , xend = x_col ,
1078+ y = y_col , yend = yend_col
1079+ )
1080+ geome <- ggplot2 :: geom_segment(
1081+ mapping = segment_mapping ,
1082+ colour = line_colour , linewidth = line_width
1083+ )
1084+ plot <- .add_geom(plot , geome )
1085+ # Start point
1086+ start_mapping <- encode(x = x_col , y = y_col )
1087+ plot <- plot | >
1088+ mark_point(mapping = start_mapping , data = d ,
1089+ colour = colour_start , size = point_size , ... )
1090+ # End point
1091+ end_mapping <- encode(x = x_col , y = yend_col )
1092+ plot <- plot | >
1093+ mark_point(mapping = end_mapping , data = d ,
1094+ colour = colour_end , size = point_size , ... )
1095+ plot
1096+ }
1097+
7821098# ---- mark_bar (hand-written: geom_col vs geom_bar dispatch) ----
7831099# ' Bar layer
7841100# '
0 commit comments