@@ -1095,6 +1095,358 @@ S7::method(mark_dumbbell, plotit_class) <- function(
10951095 plot
10961096}
10971097
1098+ # ---- mark_beeswarm ----
1099+ # ' Beeswarm plot layer
1100+ # '
1101+ # ' Adds a beeswarm (quasirandom scatter) layer to avoid overplotting
1102+ # ' for one-dimensional distributions. Requires the
1103+ # ' \pkg{ggbeeswarm} package.
1104+ # '
1105+ # ' @param plot A plotit object
1106+ # ' @param mapping Optional new aesthetics
1107+ # ' @param data Optional data for this layer
1108+ # ' @param position Position adjustment.
1109+ # ' @param method Method for point placement:
1110+ # ' `"swarm"`, `"compactswarm"`, `"hex"`, `"square"`,
1111+ # ' `"center"`, or `"centre"`.
1112+ # ' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
1113+ # ' @param rasterize_dpi DPI for rasterization (default 300).
1114+ # ' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
1115+ # ' @param ... Other arguments passed to `geom_beeswarm`
1116+ # ' @return Modified plotit object
1117+ # ' @references
1118+ # ' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/beeswarm}{Beeswarm} (corelib)
1119+ # ' @examples
1120+ # ' \donttest{
1121+ # ' if (requireNamespace("ggbeeswarm", quietly = TRUE)) {
1122+ # ' plotit(iris, encode(x = Species, y = Sepal.Length)) |>
1123+ # ' mark_beeswarm()
1124+ # ' }
1125+ # ' }
1126+ # ' @export
1127+ mark_beeswarm <- S7 :: new_generic(
1128+ " mark_beeswarm" , " plot" ,
1129+ function (plot , mapping = NULL , data = NULL , position = NULL , ... ,
1130+ method = c(" swarm" , " compactswarm" , " hex" , " square" , " center" , " centre" ),
1131+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
1132+ S7 :: S7_dispatch()
1133+ }
1134+ )
1135+
1136+ # ' @export
1137+ S7 :: method(mark_beeswarm , plotit_class ) <- function (
1138+ plot , mapping = NULL , data = NULL , position = NULL , ... ,
1139+ method = c(" swarm" , " compactswarm" , " hex" , " square" , " center" , " centre" ),
1140+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
1141+ if (! requireNamespace(" ggbeeswarm" , quietly = TRUE )) {
1142+ cli :: cli_abort(" {.fn mark_beeswarm} requires the {.pkg ggbeeswarm} package." )
1143+ }
1144+ method <- match.arg(method )
1145+ params <- rlang :: list2(... )
1146+ params $ method <- method [1 ]
1147+ do.call(function (... ) {
1148+ ._mark_impl(plot , mapping , data , position , ggbeeswarm :: geom_beeswarm ,
1149+ rasterize , rasterize_dpi , rasterize_dev , ... )
1150+ }, params )
1151+ }
1152+
1153+ # ---- mark_sankey ----
1154+ # ' Sankey flow diagram layer
1155+ # '
1156+ # ' Creates a Sankey diagram showing directed flows between nodes.
1157+ # ' Requires the \pkg{ggsankey} package. Data should contain
1158+ # ' `x`, `next_x`, `node`, and `next_node` columns as generated by
1159+ # ' \code{ggsankey::make_long}.
1160+ # '
1161+ # ' @param plot A plotit object
1162+ # ' @param mapping Optional new aesthetics. The default expects
1163+ # ' `x`, `next_x`, `node`, `next_node`, and optionally `value`.
1164+ # ' @param data Optional data for this layer
1165+ # ' @param position Position adjustment (rarely used for Sankey).
1166+ # ' @param node_colour Colour for node rectangles (default `"grey30"`).
1167+ # ' @param flow_alpha Alpha transparency for flow ribbons (default 0.5).
1168+ # ' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
1169+ # ' @param rasterize_dpi DPI for rasterization (default 300).
1170+ # ' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
1171+ # ' @param ... Other arguments passed to `geom_sankey`
1172+ # ' @return Modified plotit object
1173+ # ' @references
1174+ # ' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/sankey}{Sankey} (graphlib)
1175+ # ' @examples
1176+ # ' \donttest{
1177+ # ' if (requireNamespace("ggsankey", quietly = TRUE)) {
1178+ # ' df <- ggsankey::make_long(ggplot2::diamonds, cut, color)
1179+ # ' plotit(df, encode(x = x, next_x = next_x, node = node,
1180+ # ' next_node = next_node, value = value)) |>
1181+ # ' mark_sankey()
1182+ # ' }
1183+ # ' }
1184+ # ' @export
1185+ mark_sankey <- S7 :: new_generic(
1186+ " mark_sankey" , " plot" ,
1187+ function (plot , mapping = NULL , data = NULL , position = NULL , ... ,
1188+ node_colour = " grey30" , flow_alpha = 0.5 ,
1189+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
1190+ S7 :: S7_dispatch()
1191+ }
1192+ )
1193+
1194+ # ' @export
1195+ S7 :: method(mark_sankey , plotit_class ) <- function (
1196+ plot , mapping = NULL , data = NULL , position = NULL , ... ,
1197+ node_colour = " grey30" , flow_alpha = 0.5 ,
1198+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
1199+ if (! requireNamespace(" ggsankey" , quietly = TRUE )) {
1200+ cli :: cli_abort(" {.fn mark_sankey} requires the {.pkg ggsankey} package." )
1201+ }
1202+ if (! is.null(mapping ) && ! is.null(mapping $ colour )) {
1203+ plot <- ._clear_default_color(plot , mapping )
1204+ }
1205+ pos <- position
1206+ if (is.null(pos ) && ! is.null(plot @ meta @ dodge ) && plot @ meta @ dodge > 0 ) {
1207+ pos <- ggplot2 :: position_dodge(plot @ meta @ dodge )
1208+ }
1209+ # Sankey requires both a geom_sankey flow + a geom_sankey_text layer
1210+ # Add flow ribbon
1211+ geom_flow <- if (is.null(pos )) {
1212+ ggsankey :: geom_sankey(mapping = mapping , data = data ,
1213+ node.fill = node_colour , alpha = flow_alpha , ... )
1214+ } else {
1215+ ggsankey :: geom_sankey(mapping = mapping , data = data , position = pos ,
1216+ node.fill = node_colour , alpha = flow_alpha , ... )
1217+ }
1218+ plot <- .add_geom(plot , geom_flow ,
1219+ rasterize = rasterize , rasterize_dpi = rasterize_dpi ,
1220+ rasterize_dev = rasterize_dev
1221+ )
1222+ # Add node labels
1223+ plot <- plot | >
1224+ mark_text(mapping = mapping , data = data , repel = FALSE ,
1225+ check_overlap = FALSE , size = 3 )
1226+ plot
1227+ }
1228+
1229+ # ---- mark_treemap ----
1230+ # ' Treemap layer
1231+ # '
1232+ # ' Creates a treemap showing hierarchical data as nested rectangles.
1233+ # ' Requires the \pkg{treemapify} package. Data should contain
1234+ # ' `area`, `subgroup`, and optionally `subgroup2` columns.
1235+ # '
1236+ # ' @param plot A plotit object
1237+ # ' @param mapping Optional new aesthetics. Must include `area` for
1238+ # ' rectangle sizing.
1239+ # ' @param data Optional data for this layer
1240+ # ' @param position Position adjustment.
1241+ # ' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
1242+ # ' @param rasterize_dpi DPI for rasterization (default 300).
1243+ # ' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
1244+ # ' @param ... Other arguments passed to `geom_treemap`
1245+ # ' @return Modified plotit object
1246+ # ' @references
1247+ # ' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/treemap}{Treemap} (graphlib)
1248+ # ' @examples
1249+ # ' \donttest{
1250+ # ' if (requireNamespace("treemapify", quietly = TRUE)) {
1251+ # ' df <- data.frame(
1252+ # ' group = c("A", "B", "C"),
1253+ # ' subgroup = c("a1", "a2", "b1"),
1254+ # ' size = c(30, 20, 50))
1255+ # ' plotit(df, encode(area = size, fill = group,
1256+ # ' subgroup = subgroup)) |>
1257+ # ' mark_treemap()
1258+ # ' }
1259+ # ' }
1260+ # ' @export
1261+ mark_treemap <- S7 :: new_generic(
1262+ " mark_treemap" , " plot" ,
1263+ function (plot , mapping = NULL , data = NULL , position = NULL , ... ,
1264+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
1265+ S7 :: S7_dispatch()
1266+ }
1267+ )
1268+
1269+ # ' @export
1270+ S7 :: method(mark_treemap , plotit_class ) <- function (
1271+ plot , mapping = NULL , data = NULL , position = NULL , ... ,
1272+ rasterize = FALSE , rasterize_dpi = 300 , rasterize_dev = " cairo" ) {
1273+ if (! requireNamespace(" treemapify" , quietly = TRUE )) {
1274+ cli :: cli_abort(" {.fn mark_treemap} requires the {.pkg treemapify} package." )
1275+ }
1276+ if (! is.null(mapping ) && ! is.null(mapping $ fill )) {
1277+ plot <- ._clear_default_color(plot , mapping )
1278+ }
1279+ pos <- position
1280+ if (is.null(pos ) && ! is.null(plot @ meta @ dodge ) && plot @ meta @ dodge > 0 ) {
1281+ pos <- ggplot2 :: position_dodge(plot @ meta @ dodge )
1282+ }
1283+ geom <- if (is.null(pos )) {
1284+ treemapify :: geom_treemap(mapping = mapping , data = data , ... )
1285+ } else {
1286+ treemapify :: geom_treemap(mapping = mapping , data = data , position = pos , ... )
1287+ }
1288+ plot <- .add_geom(plot , geom ,
1289+ rasterize = rasterize , rasterize_dpi = rasterize_dpi ,
1290+ rasterize_dev = rasterize_dev
1291+ )
1292+ plot
1293+ }
1294+
1295+ # ---- mark_network ----
1296+ # ' Network / force-directed graph layer
1297+ # '
1298+ # ' Creates a network visualization with nodes and edges.
1299+ # ' Requires the \pkg{ggraph} and \pkg{igraph} packages.
1300+ # '
1301+ # ' @param plot A plotit object. The data should be an `igraph`
1302+ # ' object.
1303+ # ' @param layout Layout algorithm: `"auto"` (default, uses
1304+ # ' `layout_with_fr`), `"circle"`, `"linear"`, `"bipartite"`,
1305+ # ' or `"manual"`.
1306+ # ' @param edge_colour Colour for edges (default `"grey70"`).
1307+ # ' @param edge_width Width for edges (default 0.5).
1308+ # ' @param node_colour Fill colour for nodes (default `"#4E79A7"`).
1309+ # ' @param node_size Size for nodes (default 5).
1310+ # ' @param ... Other arguments passed to `ggraph::geom_edge_link`
1311+ # ' and `ggraph::geom_node_point`
1312+ # ' @return Modified plotit object
1313+ # ' @references
1314+ # ' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/force-graph}{ForceGraph} (graphlib)
1315+ # ' @examples
1316+ # ' \donttest{
1317+ # ' if (requireNamespace("ggraph", quietly = TRUE) &&
1318+ # ' requireNamespace("igraph", quietly = TRUE)) {
1319+ # ' gr <- igraph::sample_pa(30)
1320+ # ' plotit(gr, encode()) |> mark_network()
1321+ # ' }
1322+ # ' }
1323+ # ' @export
1324+ mark_network <- S7 :: new_generic(
1325+ " mark_network" , " plot" ,
1326+ function (plot ,
1327+ layout = c(" auto" , " circle" , " linear" , " bipartite" , " manual" ),
1328+ edge_colour = " grey70" , edge_width = 0.5 ,
1329+ node_colour = " #4E79A7" , node_size = 5 , ... ) {
1330+ S7 :: S7_dispatch()
1331+ }
1332+ )
1333+
1334+ # ' @export
1335+ S7 :: method(mark_network , plotit_class ) <- function (
1336+ plot ,
1337+ layout = c(" auto" , " circle" , " linear" , " bipartite" , " manual" ),
1338+ edge_colour = " grey70" , edge_width = 0.5 ,
1339+ node_colour = " #4E79A7" , node_size = 5 , ... ) {
1340+ if (! requireNamespace(" ggraph" , quietly = TRUE )) {
1341+ cli :: cli_abort(" {.fn mark_network} requires the {.pkg ggraph} package." )
1342+ }
1343+ if (! requireNamespace(" igraph" , quietly = TRUE )) {
1344+ cli :: cli_abort(" {.fn mark_network} requires the {.pkg igraph} package." )
1345+ }
1346+ layout <- match.arg(layout )
1347+ graph_data <- plot @ gg $ data
1348+ if (! inherits(graph_data , " igraph" )) {
1349+ cli :: cli_abort(
1350+ " {.fn mark_network} requires an {.cls igraph} object as plot data."
1351+ )
1352+ }
1353+ layout_fun <- switch (layout ,
1354+ auto = igraph :: layout_with_fr ,
1355+ circle = igraph :: layout_in_circle ,
1356+ linear = igraph :: layout_on_line ,
1357+ bipartite = igraph :: layout_as_bipartite ,
1358+ manual = igraph :: layout_nicely
1359+ )
1360+ gg <- ggraph :: ggraph(graph_data , layout = layout_fun )
1361+ gg <- gg + ggplot2 :: theme_void()
1362+ # Edge layer
1363+ gg <- gg + ggraph :: geom_edge_link(edge_colour = edge_colour ,
1364+ edge_width = edge_width , ... )
1365+ # Node layer
1366+ gg <- gg + ggraph :: geom_node_point(fill = node_colour , size = node_size )
1367+ plot @ gg <- gg
1368+ plot
1369+ }
1370+
1371+ # ---- mark_chord ----
1372+ # ' Chord diagram layer
1373+ # '
1374+ # ' Creates a chord diagram showing pairwise relationships between groups.
1375+ # ' Requires the \pkg{circlize} package. Data should be an adjacency matrix
1376+ # ' or a data frame with `from`, `to`, and `value` columns.
1377+ # '
1378+ # ' @param plot A plotit object
1379+ # ' @param gap_width Gap between sectors in degrees (default 4).
1380+ # ' @param grid_colour Colour for the outer grid (default `"grey80"`).
1381+ # ' @param link_colour Colour for the chord links (default `"grey30"`).
1382+ # ' @param link_alpha Alpha transparency for links (default 0.5).
1383+ # ' @param ... Other arguments passed to `circlize::chordDiagram`
1384+ # ' @return Modified plotit object
1385+ # ' @references
1386+ # ' AntV G2: \href{https://g2.antv.antgroup.com/en/api/mark/chord}{Chord} (graphlib)
1387+ # ' @examples
1388+ # ' \donttest{
1389+ # ' if (requireNamespace("circlize", quietly = TRUE)) {
1390+ # ' mat <- matrix(c(0, 5, 3, 2, 0, 4, 1, 3, 0), nrow = 3)
1391+ # ' rownames(mat) <- colnames(mat) <- c("A", "B", "C")
1392+ # ' plotit(as.data.frame(as.table(mat)), encode()) |>
1393+ # ' mark_chord()
1394+ # ' }
1395+ # ' }
1396+ # ' @export
1397+ mark_chord <- S7 :: new_generic(
1398+ " mark_chord" , " plot" ,
1399+ function (plot , gap_width = 4 , grid_colour = " grey80" ,
1400+ link_colour = " grey30" , link_alpha = 0.5 , ... ) {
1401+ S7 :: S7_dispatch()
1402+ }
1403+ )
1404+
1405+ # ' @export
1406+ S7 :: method(mark_chord , plotit_class ) <- function (
1407+ plot , gap_width = 4 , grid_colour = " grey80" ,
1408+ link_colour = " grey30" , link_alpha = 0.5 , ... ) {
1409+ if (! requireNamespace(" circlize" , quietly = TRUE )) {
1410+ cli :: cli_abort(" {.fn mark_chord} requires the {.pkg circlize} package." )
1411+ }
1412+ d <- plot @ gg $ data
1413+ # Expect adjacency matrix or from-to-value data frame
1414+ if (is.data.frame(d )) {
1415+ if (all(c(" Var1" , " Var2" , " Freq" ) %in% names(d ))) {
1416+ # Convert table-as-dataframe to matrix
1417+ mat <- xtabs(Freq ~ Var1 + Var2 , data = d )
1418+ } else if (all(c(" from" , " to" , " value" ) %in% names(d ))) {
1419+ mat <- xtabs(value ~ from + to , data = d )
1420+ } else {
1421+ cli :: cli_abort(
1422+ " {.fn mark_chord} expects data with columns Var1/Var2/Freq or from/to/value."
1423+ )
1424+ }
1425+ } else if (is.matrix(d )) {
1426+ mat <- d
1427+ } else {
1428+ cli :: cli_abort(
1429+ " {.fn mark_chord} expects a matrix or data frame as plot data."
1430+ )
1431+ }
1432+ # Build a new ggplot with a custom drawing layer
1433+ chord_grob <- function (... ) {
1434+ circlize :: chordDiagram(mat , ... )
1435+ }
1436+ # Use annotation layer since circlize draws directly
1437+ gg <- ggplot2 :: ggplot() + ggplot2 :: theme_void()
1438+ plot @ gg <- gg
1439+ # Draw chord via a custom annotation
1440+ circlize :: chordDiagram(mat ,
1441+ transparency = 1 - link_alpha ,
1442+ grid.col = grid_colour ,
1443+ annotationTrack = " grid" ,
1444+ preAllocateTracks = list (track.height = 0.1 ),
1445+ ...
1446+ )
1447+ plot
1448+ }
1449+
10981450# ---- mark_bar (hand-written: geom_col vs geom_bar dispatch) ----
10991451# ' Bar layer
11001452# '
0 commit comments