Introduction
In the previous post, we introduced ports in g6R, which brought precise connection points to nodes for building interactive data workflows. In this post, we look at another new feature: collapsible nodes and combos.
As graphs grow in size, they can quickly become hard to read. Collapsible nodes let you hide parts of the graph behind a single node, reducing visual clutter while preserving the full structure underneath. Clicking a collapse button on a parent node hides all of its children and clicking again restores them. The same mechanism works on combos, which are visual containers that group nodes together.
To get started, install the latest development version:
pak::pak("cynkra/g6R")
New features
The improvements presented here do not replace the existing
collapse_expand() behavior, which handles combos and nodes.
collapse_expand() works well for tree structures but falls short with
DAGs, where a child can have multiple parents. Our approach is designed
to handle these more complex topologies correctly.
Collapsible nodes
g6_node() now accepts a collapse parameter configured via
g6_collapse_options(). This works with any of the custom-*-node
types for instance custom-rect-node or custom-circle-node. When a
node has children defined, a character vector of child node IDs, it
displays a collapse button:
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "dataset: iris"),
children = c("b", "c"),
collapse = g6_collapse_options(collapsed = TRUE)
)
The g6_collapse_options() function provides full control over the
button’s appearance and behavior:
g6_collapse_options(
collapsed = FALSE, # initial state
visibility = "visible", # "visible" or "hover"
placement = "right-top", # preset name or c(x, y) coordinates
r = 6, # button radius
fill = "#fff", # background color
stroke = "#9cabd4", # border color
iconStroke = "#9cabd4" # plus/minus icon color
)
When a node is collapsed, its children are hidden and a badge shows the count of hidden nodes. Edges connected to hidden children are rerouted to the collapsed parent so the graph remains readable.
Here is a minimal example with three nodes where a can collapse its
two children:
library(shiny)
library(g6R)
ui <- fluidPage(
g6_output("graph", height = "400px")
)
server <- function(input, output, session) {
output$graph <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "Parent"),
children = c("b", "c"),
collapse = g6_collapse_options()
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "Child 1")
),
g6_node(
id = "c",
type = "custom-rect-node",
style = list(labelText = "Child 2")
)
),
edges = g6_edges(
g6_edge(source = "a", target = "b"),
g6_edge(source = "a", target = "c")
)
) |>
g6_layout(antv_dagre_layout()) |>
g6_behaviors(drag_element(), zoom_canvas())
)
}
shinyApp(ui, server)


In a DAG, a child node can have multiple parents. When two parents share common children, collapsing one parent also collapses the other to keep the graph state consistent. In the screenshot below, both the Head node (combo 1) and the Subset node (combo 2) share downstream children. Collapsing Head automatically collapses Subset as well, both showing a “+4” badge for their hidden descendants.

Automatic parent/child tracking
When a node has children set, g6R automatically sets the
g6R.directed_graph option to TRUE. This means that when you create a
connection between two nodes (via create_edge() or proxy functions),
the parent/child relationship is automatically established. Likewise,
when an edge or node is removed through g6_remove_edges() or
g6_remove_nodes(), the relationship is cleaned up. Importantly, the
parent/child tracking only works when using g6R proxy functions. Calling
the G6 JS API directly (e.g. graph.removeEdgeData(...)) will not keep
the tree state in sync.
Controlling collapse depth
set_g6_max_collapse_depth() controls which nodes display a collapse
button based on their depth in the graph. Depth is computed
automatically from the parent/child relationships. Root nodes, those
with no parent, have depth 0. Their direct children have depth 1, and so
on. Only nodes whose depth is less than or equal to maxCollapseDepth
will show a collapse button.
The three key values are:
-
Inf(default): all nodes with children are collapsible, regardless of how deep they are in the hierarchy. -
0: only root nodes show collapse buttons. This is useful when you want a single top-level toggle that collapses everything below, without allowing intermediate nodes to be collapsed independently. -
-1: collapsing is entirely disabled. Collapse buttons are removed from all nodes, even ifg6_collapse_options()is provided and nodes have children. This can be useful if you think the graph complexity is manageable without collapsing.
For example, consider a graph with three levels: a -> b -> c. With
set_g6_max_collapse_depth(0), only node a (depth 0) would show a
collapse button. Node b (depth 1) would not, even though it has a
child.
The two apps below illustrate the difference. Both use the same
three-level graph a -> b -> c, but with different depth limits.
With set_g6_max_collapse_depth(Inf), both a and b show a collapse
button:
library(shiny)
library(g6R)
set_g6_max_collapse_depth(Inf)
ui <- fluidPage(
g6_output("graph", height = "400px")
)
server <- function(input, output, session) {
output$graph <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "a (depth 0)"),
children = "b",
collapse = g6_collapse_options()
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "b (depth 1)"),
children = "c",
collapse = g6_collapse_options()
),
g6_node(
id = "c",
type = "custom-rect-node",
style = list(labelText = "c (depth 2)")
)
),
edges = g6_edges(
g6_edge(source = "a", target = "b"),
g6_edge(source = "b", target = "c")
)
) |>
g6_layout(antv_dagre_layout()) |>
g6_behaviors(drag_element(), zoom_canvas())
)
}
shinyApp(ui, server)

With set_g6_max_collapse_depth(0), only node a (depth 0) shows a
collapse button. Node b does not, even though it has a child:
library(shiny)
library(g6R)
set_g6_max_collapse_depth(0)
ui <- fluidPage(
g6_output("graph", height = "400px")
)
server <- function(input, output, session) {
output$graph <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "a (depth 0)"),
children = "b",
collapse = g6_collapse_options()
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "b (depth 1)"),
children = "c",
collapse = g6_collapse_options()
),
g6_node(
id = "c",
type = "custom-rect-node",
style = list(labelText = "c (depth 2)")
)
),
edges = g6_edges(
g6_edge(source = "a", target = "b"),
g6_edge(source = "b", target = "c")
)
) |>
g6_layout(antv_dagre_layout()) |>
g6_behaviors(drag_element(), zoom_canvas())
)
}
shinyApp(ui, server)
You can also call options(g6R.max_collapse_depth) directly, even if
the latter does not validate the passed value.
Collapsible combos
This feature was already available in previous versions but has been
substantially improved in this release. g6_combo() also supports the
collapse parameter with g6_collapse_options(). When collapse is
provided and type is NULL, the combo type is automatically set to
"rect-combo-with-extra-button":
g6_combo(
"combo1",
collapse = g6_collapse_options(
placement = "bottom",
fill = "#f0f0f0",
visibility = "hover"
)
)
The combo collapse button supports all the same styling options as nodes such as configurable placement, radius, fill, stroke, icon stroke, and visibility modes.
The following example shows a combo containing two nodes where a is
the parent of b. Both the combo and node a are collapsible. When you
collapse node a, its child b is hidden and the combo automatically
resizes to fit the remaining visible node:
library(shiny)
library(g6R)
ui <- fluidPage(
g6_output("graph", height = "400px")
)
server <- function(input, output, session) {
output$graph <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "Parent"),
combo = "combo1",
children = "b",
collapse = g6_collapse_options()
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "Child"),
combo = "combo1"
)
),
edges = g6_edges(
g6_edge(source = "a", target = "b")
),
combos = g6_combos(
g6_combo(
"combo1",
collapse = g6_collapse_options(placement = "right")
)
)
) |>
g6_layout(antv_dagre_layout()) |>
g6_options(
renderer = JS("() => new SVGRenderer()")
) |>
g6_behaviors(
drag_element(),
zoom_canvas()
)
)
}
shinyApp(ui, server)



Combo virtual edges
The next example shows a more realistic setup with two combos and cross-combo connections. When collapsing a combo, we show virtual edges from the combo to any nodes outside that are connected to hidden children. This way you can still see how the collapsed content relates to the rest of the graph:
library(blockr.dag)
library(blockr.core)
library(blockr.dock)
options(
"g6R.mode" = "dev" #,
#"g6R.layout_on_data_change" = TRUE
)
serve(
new_dock_board(
blocks = c(
a = new_dataset_block("iris"),
b = new_head_block(n = 10),
c = new_subset_block(),
d = new_head_block(n = 5),
e = new_rbind_block(),
f = new_subset_block(),
g = new_rbind_block(),
h = new_head_block(n = 3),
i = new_scatter_block(x = "Sepal.Length", y = "Sepal.Width")
),
links = c(
new_link(from = "a", to = "b", input = "data"),
new_link(from = "b", to = "c", input = "data"),
new_link(from = "c", to = "d", input = "data"),
new_link(from = "b", to = "f", input = "data"),
new_link(from = "d", to = "e", input = "1"),
new_link(from = "f", to = "e", input = "2"),
new_link(from = "e", to = "g", input = "1"),
new_link(from = "f", to = "g", input = "2"),
new_link(from = "g", to = "h", input = "data"),
new_link(from = "h", to = "i", input = "data")
),
stacks = c(
new_stack(id = "stack1", blocks = c("b", "c", "d")),
new_stack(id = "stack2", blocks = c("f", "g"))
),
extensions = new_dag_extension()
)
)


Overlay plugin improvements
bubble_sets() and hull() now automatically set
pointerEvents = "none" and zIndex = -1 by default. This fixes two
issues when using the SVG renderer:
- Overlay shapes no longer block pointer events like drag and click on nodes.
- They render behind nodes so they don’t visually cover collapse buttons or other node UI.
These defaults can be overridden by passing explicit values.
Additionally, overlay plugins now resize dynamically when nodes are collapsed or uncollapsed. Hidden members are temporarily removed from the overlay shape and restored when expanded again.
The following example uses bubble_sets() to group three nodes.
Collapsing node a hides b and c, and the bubble set shrinks to
wrap only the remaining visible node:
library(shiny)
library(g6R)
ui <- fluidPage(
g6_output("graph", height = "400px")
)
server <- function(input, output, session) {
output$graph <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "a"),
children = c("b", "c"),
collapse = g6_collapse_options()
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "b")
),
g6_node(
id = "c",
type = "custom-rect-node",
style = list(labelText = "c")
)
),
edges = g6_edges(
g6_edge(source = "a", target = "b"),
g6_edge(source = "a", target = "c")
)
) |>
g6_layout(antv_dagre_layout()) |>
g6_options(
renderer = JS("() => new SVGRenderer()")
) |>
g6_behaviors(drag_element(), zoom_canvas()) |>
g6_plugins(
bubble_sets(
members = c("a", "b", "c"),
label = TRUE,
labelText = "bubble set",
fill = "#F08F56",
stroke = "#F08F56",
labelBackground = TRUE,
labelPlacement = "top",
labelFill = "#fff",
labelPadding = 2,
labelBackgroundFill = "#F08F56",
labelBackgroundRadius = 5
)
)
)
}
shinyApp(ui, server)

The same applies to hull(). In this example, collapsing node a hides
its children and the hull resizes accordingly:
library(shiny)
library(g6R)
ui <- fluidPage(
g6_output("graph", height = "400px")
)
server <- function(input, output, session) {
output$graph <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "a"),
children = c("b", "c"),
collapse = g6_collapse_options()
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "b")
),
g6_node(
id = "c",
type = "custom-rect-node",
style = list(labelText = "c")
)
),
edges = g6_edges(
g6_edge(source = "a", target = "b"),
g6_edge(source = "a", target = "c")
)
) |>
g6_layout(antv_dagre_layout()) |>
g6_options(
renderer = JS("() => new SVGRenderer()")
) |>
g6_behaviors(drag_element(), zoom_canvas()) |>
g6_plugins(
hull(
members = c("a", "b", "c"),
labelText = "hull",
fill = "#7B68EE",
stroke = "#7B68EE",
labelBackground = TRUE,
labelPlacement = "top",
labelFill = "#fff",
labelPadding = 2,
labelBackgroundFill = "#7B68EE",
labelBackgroundRadius = 5
)
)
)
}
shinyApp(ui, server)

Example
The Shiny following application summarises all the listed features. The context menu allows you to remove edges and nodes, and see how parent/child relationships are automatically maintained.
library(shiny)
library(g6R)
options(
"g6R.mode" = "dev",
"g6R.max_collapse_depth" = Inf,
"g6R.directed_graph" = TRUE
)
ui <- fluidPage(
g6_output("dag", height = "1000px"),
verbatimTextOutput("clicked_port"),
verbatimTextOutput("removed_node")
)
server <- function(input, output, session) {
output$dag <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = "a",
type = "custom-rect-node",
style = list(labelText = "dataset: iris"),
ports = g6_ports(
g6_output_port(
key = "output-a",
placement = "bottom",
label = "data"
)
),
children = c("b"),
collapse = g6_collapse_options(
collapsed = TRUE,
visibility = "hover",
placement = "right-top"
)
),
g6_node(
id = "b",
type = "custom-rect-node",
style = list(labelText = "head(n=10)"),
ports = g6_ports(
g6_input_port(
key = "input-b",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-b",
placement = "bottom",
label = "data"
)
),
combo = "combo1",
children = c("c", "f"),
collapse = g6_collapse_options(placement = "right-top")
),
g6_node(
id = "c",
type = "custom-rect-node",
style = list(labelText = "subset"),
ports = g6_ports(
g6_input_port(
key = "input-c",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-c",
placement = "bottom",
label = "data"
)
),
combo = "combo1",
children = "d"
),
g6_node(
id = "d",
type = "custom-rect-node",
style = list(labelText = "head(n=5)"),
ports = g6_ports(
g6_input_port(
key = "input-d",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-d",
placement = "bottom",
label = "data"
)
),
combo = "combo1",
children = c("e"),
collapse = g6_collapse_options(
collapsed = FALSE,
placement = "right-top"
)
),
g6_node(
id = "e",
type = "custom-rect-node",
style = list(labelText = "rbind"),
ports = g6_ports(
g6_input_port(
key = "input-e-1",
placement = "top",
label = "1",
arity = Inf
),
g6_input_port(
key = "input-e-2",
placement = "top",
label = "2",
arity = Inf
),
g6_output_port(
key = "output-e",
placement = "bottom",
label = "data",
arity = Inf
)
),
children = c("g"),
collapse = g6_collapse_options(
collapsed = FALSE,
placement = "right-top"
)
),
g6_node(
id = "f",
type = "custom-rect-node",
style = list(labelText = "subset"),
ports = g6_ports(
g6_input_port(
key = "input-f",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-f",
placement = "bottom",
label = "data"
)
),
combo = "combo2",
children = c("e", "g"),
collapse = g6_collapse_options(
collapsed = FALSE,
placement = "right-top"
)
),
g6_node(
id = "g",
type = "custom-rect-node",
style = list(labelText = "rbind"),
ports = g6_ports(
g6_input_port(
key = "input-g-1",
placement = "top",
label = "1",
arity = Inf
),
g6_input_port(
key = "input-g-2",
placement = "top",
label = "2",
arity = Inf
),
g6_output_port(
key = "output-g",
placement = "bottom",
label = "data",
arity = Inf
)
),
combo = "combo2",
children = c("h"),
collapse = g6_collapse_options(
collapsed = FALSE,
placement = "right-top"
)
),
g6_node(
id = "h",
type = "custom-rect-node",
style = list(labelText = "head(n=3)"),
ports = g6_ports(
g6_input_port(
key = "input-h",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-h",
placement = "bottom",
label = "data"
)
),
children = c("i"),
collapse = g6_collapse_options(
collapsed = FALSE,
placement = "right-top"
)
),
g6_node(
id = "i",
type = "custom-rect-node",
style = list(labelText = "scatter plot"),
ports = g6_ports(
g6_input_port(
key = "input-i",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-i",
placement = "bottom",
label = "data"
)
),
children = c("j"),
collapse = g6_collapse_options(placement = "right-top")
),
g6_node(
id = "j",
type = "custom-rect-node",
style = list(labelText = "filter"),
ports = g6_ports(
g6_input_port(
key = "input-j",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-j",
placement = "bottom",
label = "data"
)
),
children = c("k", "l"),
collapse = g6_collapse_options(placement = "right-top")
),
g6_node(
id = "k",
type = "custom-rect-node",
style = list(labelText = "summarise"),
ports = g6_ports(
g6_input_port(
key = "input-k",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-k",
placement = "bottom",
label = "data"
)
),
children = c("m"),
collapse = g6_collapse_options(placement = "right-top")
),
g6_node(
id = "l",
type = "custom-rect-node",
style = list(labelText = "mutate"),
ports = g6_ports(
g6_input_port(
key = "input-l",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-l",
placement = "bottom",
label = "data"
)
),
children = c("n"),
collapse = g6_collapse_options(placement = "right-top")
),
g6_node(
id = "m",
type = "custom-rect-node",
style = list(labelText = "arrange"),
ports = g6_ports(
g6_input_port(
key = "input-m",
placement = "top",
label = "data"
),
g6_output_port(
key = "output-m",
placement = "bottom",
label = "data"
)
)
),
g6_node(
id = "n",
type = "custom-rect-node",
style = list(labelText = "select"),
ports = g6_ports(
g6_input_port(
key = "input-n",
placement = "top",
label = "data"
)
)
)
),
edges = g6_edges(
g6_edge(
source = "a", target = "b",
style = list(
sourcePort = "output-a", targetPort = "input-b",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "b", target = "c",
style = list(
sourcePort = "output-b", targetPort = "input-c",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "c", target = "d",
style = list(
sourcePort = "output-c", targetPort = "input-d",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "b", target = "f",
style = list(
sourcePort = "output-b", targetPort = "input-f",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "d", target = "e",
style = list(
sourcePort = "output-d", targetPort = "input-e-1",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "f", target = "e",
style = list(
sourcePort = "output-f", targetPort = "input-e-2",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "e", target = "g",
style = list(
sourcePort = "output-e", targetPort = "input-g-1",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "f", target = "g",
style = list(
sourcePort = "output-f", targetPort = "input-g-2",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "g", target = "h",
style = list(
sourcePort = "output-g", targetPort = "input-h",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "h", target = "i",
style = list(
sourcePort = "output-h", targetPort = "input-i",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "i", target = "j",
style = list(
sourcePort = "output-i", targetPort = "input-j",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "j", target = "k",
style = list(
sourcePort = "output-j", targetPort = "input-k",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "j", target = "l",
style = list(
sourcePort = "output-j", targetPort = "input-l",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "k", target = "m",
style = list(
sourcePort = "output-k", targetPort = "input-m",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
),
g6_edge(
source = "l", target = "n",
style = list(
sourcePort = "output-l", targetPort = "input-n",
endArrow = TRUE, startArrow = FALSE, endArrowType = "vee"
)
)
),
combos = g6_combos(
g6_combo(
"combo1",
collapse = g6_collapse_options(visibility = "hover")
),
g6_combo(
"combo2",
collapse = g6_collapse_options()
)
)
) |>
g6_layout(antv_dagre_layout(sortByCombo = TRUE)) |>
g6_options(
animation = FALSE,
autoFit = TRUE,
renderer = JS("() => new SVGRenderer()"),
edge = list(style = list(endArrow = TRUE))
) |>
g6_behaviors(
collapse_expand(
enable = JS("(e) => e.targetType === 'combo'")
),
click_select(multiple = TRUE),
drag_element(),
drag_canvas(
enable = JS(
"(e) => {
return e.targetType === 'canvas' && !e.shiftKey && !e.altKey;
}"
)
),
zoom_canvas(),
create_edge(
enable = JS(
"(e) => {
return e.targetType === 'node' && e.targetType !== 'combo';
}"
),
onFinish = NULL
)
) |>
g6_plugins(
bubble_sets(
key = "bubble-set-1",
members = c("j", "k"),
label = TRUE,
labelText = "bubble set",
fill = "#F08F56",
stroke = "#F08F56",
labelBackground = TRUE,
labelPlacement = "top",
labelFill = "#fff",
labelPadding = 2,
labelBackgroundFill = "#F08F56",
labelBackgroundRadius = 5
),
hull(
key = "hull-1",
members = c("l", "m", "n"),
labelText = "Super hull",
labelAutoRotate = FALSE,
labelCloseToPath = FALSE,
fill = "#7B68EE",
stroke = "#7B68EE",
labelBackground = TRUE,
labelPlacement = "top",
labelFill = "#fff",
labelPadding = 2,
labelBackgroundFill = "#7B68EE",
labelBackgroundRadius = 5
),
context_menu(
enable = JS("(e) => true"),
getItems = JS(
"(e) => {
if (e.targetType === 'edge') {
return [{ name: 'Remove edge', value: 'remove_edge' }];
} else if (e.targetType === 'node') {
return [{ name: 'Remove node', value: 'remove_node' }];
}
return [];
}"
),
onClick = JS(
"(value, target, current) => {
const graph = HTMLWidgets
.find(`#${target.closest('.g6').id}`)
.getWidget();
if (current.id === undefined) return;
if (value === 'remove_edge') {
Shiny.setInputValue(
target.closest('.g6').id + '-removed_edge',
{id: current.id},
{priority: 'event'}
);
} else if (value === 'remove_node') {
Shiny.setInputValue(
target.closest('.g6').id + '-removed_node',
{id: current.id},
{priority: 'event'}
);
}
}"
)
)
)
)
output$clicked_port <- renderPrint({
input[["dag-selected_port"]]
})
output$removed_node <- renderPrint({
input[["dag-removed_node"]]
})
proxy <- g6_proxy("dag")
observeEvent(input[["dag-selected_port"]], {
new_id <- as.character(round(as.numeric(Sys.time())))
pos <- input[["dag-mouse_position"]]
proxy |>
g6_add_nodes(
g6_node(
id = new_id,
type = "custom-circle-node",
style = list(
x = pos$x + 50,
y = pos$y,
labelText = paste("Node", new_id)
),
ports = g6_ports(
g6_input_port(
key = sprintf("input-%s", new_id),
placement = "left",
arity = Inf
),
g6_output_port(
key = sprintf("output-%s", new_id),
placement = "right",
arity = Inf
)
)
)
) |>
g6_add_edges(
g6_edge(
source = as.character(
input[["dag-selected_port"]][["node"]]
),
target = new_id,
style = list(
sourcePort = input[["dag-selected_port"]][["port"]],
targetPort = sprintf("input-%s", new_id),
endArrow = TRUE
)
)
)
})
observeEvent(input[["dag-removed_edge"]], {
proxy |>
g6_remove_edges(input[["dag-removed_edge"]]$id)
})
observeEvent(input[["dag-removed_node"]], {
proxy |>
g6_remove_nodes(input[["dag-removed_node"]]$id)
})
}
shinyApp(ui, server)
Conclusion
This feature was developed by supervising an AI coding agent (Claude) through a structured workflow. Before writing any code, we prepared a set of files capturing motivations, requirements, design decisions and implementation details. This upfront planning helped the AI understand the existing codebase and produce code that respects its conventions, rather than introducing a foreign style. While the AI handled the bulk of the implementation across many files, human review and adjustements remained essential to catch edge cases and ensure the result integrates cleanly with the rest of g6R.
Next steps
If you have feedback, reach out via the contact page on this site. You can also report issues or feature requests on the g6R repository.