Introduction
blockr is a new tool to build data analyses and apps in minutes, using a point and click user interface. Central to this new tool is a block-based pipeline builder which allows you to specify your analyses as a series of connected nodes in a graph. In this blog post we are going to dive into blockr.dag, an extension that powers this blockr graph, and g6R, the R package which powers this extension.

To get started quickly, install the latest development versions with:
# Latest GitHub main branches
pak::pak("cynkra/g6R")
pak::pak("BristolMyersSquibb/blockr.dag")
What are ports?
g6R 0.6.0 (preview) introduces ports, which are connection points displayed on the borders of nodes. You can think of ports like the ports on the back of a computer motherboard, where you connect cables or peripherals such as USB ports.

Ports are labeled so you know where to connect elements, just as your computer needs the right connections to function. In data science, ports are ideal for organizing complex data flows.
Using ports in blockr.dag
Block categories and port arities
In blockr, there are three main categories of blocks:
- Input blocks, where data enters the workflow (e.g., dataset block).
- Transformation blocks, which modify data (e.g., filter rows block).
- Plot blocks, which visualize data (e.g., ggplot2 block).
All blocks have a single output port, which outputs raw or transformed data. This port has infinite arity, meaning it can connect to multiple input ports on other blocks.
Blocks may have input ports with different arities. For example, the
transformation Join block has two input ports—one for the left table
and one for the right table. Each of these ports has an arity of one, so
they can only connect to a single output port from another block.

Other blocks, such as bind rows or columns, have input ports with infinite arity, allowing them to connect to multiple output ports and bind any number of tables together. Entry-point blocks, like the dataset block, have no input ports since they are the starting point for data.
Connecting ports
There are three ways to connect ports in a blockr application:
- Click on the port guide and follow the instructions in the app. Hovering over a free port displays an arrow cursor indicating the drag direction.
- Drag and drop from one port to another. In blockr.dag, you must hold ‘Shift’ while dragging to connect ports.
- Right click on a block to open the context menu and select ‘Create link’. This connects the current node (with only one output port), to a selected port of another block.

Once connected, data can flow from one block to another. If you hover over a port that has reached its maximum number of connections, the port guide changes to indicate the port is blocked. You cannot connect to this port unless you remove existing connections.

How to create ports with g6R
Now we have seen how ports work in blockr, let’s jump into some of the technical details and see how to define ports with g6R.
Define ports
To enable ports, pass a custom type to g6_node(), such as
custom-circle-node or custom-rect-node. Nine
shapes
are compatible with ports, except HTML. The g6_node() function now has
a ports argument to define ports for each node. In the g6 JS library,
ports are usually defined inside style, but we consider them important
enough to define directly in the node data. g6R automatically moves them
to style.ports when rendering the graph. That said, if you already
were using ports within style, they should still work.
Use the g6_port() function to create ports and wrap them in
g6_ports():
g6_port(
key = "key",
label = "Port label",
type = "input",
arity = 1,
placement = "left",
fill = "#52C41A",
r = 4
)
A port has a unique key, a type (either input or output), an
arity (number of connections allowed), and other style parameters
inherited from
g6.
g6R automatically makes port keys unique by prefixing them with the node
ID on the JS side. We added a label parameter (not in the G6 API) to
display a tooltip when hovering over the port. Port placement is
controlled by the placement argument, which supports 6 values: top,
bottom, left, right, top-left, top-right, bottom-left,
bottom-right, or a set of (x, y) coordinates (0, 0 is the top-left
corner). Port visiblity is controlled with the visibility parameter
which can be either visible (default), hover or hidden. Ports can
only be displayed on the node border; invalid coordinates will cause an
error. Combining input[["<GRAPH_ID>-selected_port"]] and
input[["<graph_ID>-mouse_position"]], you can add and connect nodes on
the fly at the guide location (see example below).
For use in other packages like blockr.dag, two helper functions exist:
-
Input ports (
g6_input_port()): can only be the target of an edge. -
Output ports (
g6_output_port()): can only be the source of an edge.
Connect ports with edges
When creating edges, provide sourcePort and/or targetPort within the
style list to connect to the corresponding ports. Validation ensures
you cannot connect incompatible ports (e.g., output to output, or a port
to itself):
g6_edge(
source = 1,
target = 2,
style = list(
sourcePort = "output-1",
targetPort = "input-2",
endArrow = TRUE,
startArrow = FALSE,
endArrowType = "vee"
)
)
The create_edge() behavior has also been improved to work better with
ports. For example, you cannot drag from a port that has reached its
arity limit, or from a node with ports (drag from the ports instead).
Update ports
Ports can be updated from the server with g6_update_ports(), which
supports three actions, namely remove, add, or update:
g6_update_ports(
g6_proxy("graph"),
ids = c("A", "B"),
ops = list(
A = list(remove = c("out1", "out2")),
B = list(
add = list(g6_port(key = "new", label = "new", placement = "top")),
update = list(g6_port(key = "in2", label = "Updated label"))
)
)
)
idsrefers to existing node IDs and ops is a list of possible
operations per node. When the action is remove, we pass a vector of
ports to be removed. For add and update, we pass a list of
g6_port() objects. The Shiny application below shows how to leverage
g6_update_ports():
library(shiny)
library(g6R)
ui <- fluidPage(
g6_output("graph", height = "500px"),
actionButton("update_ports", "Update Ports")
)
server <- function(input, output, session) {
output$graph <- render_g6({
g6(
nodes = g6_nodes(
g6_node(
id = "A",
ports = g6_ports(
g6_input_port(key = "in1", label = "in1", placement = "left"),
g6_output_port(key = "out1", label = "out1", placement = "right"),
g6_output_port(key = "out2", label = "out2", placement = c(1, 0.7))
),
style = list(x = 100, y = 200, labelText = "Node A")
),
g6_node(
id = "B",
ports = g6_ports(
g6_input_port(key = "in2", label = "in2", placement = "left"),
g6_output_port(key = "out3", label = "out3", placement = "right"),
g6_output_port(key = "out4", label = "out4", placement = c(1, 0.3))
),
style = list(x = 300, y = 200, labelText = "Node B")
)
),
edges = g6_edges(
g6_edge(source = "A", target = "B", style = list(sourcePort = "out1", targetPort = "in2"))
)
) |> g6_behaviors(click_select(), drag_element(), drag_canvas())
})
observeEvent(input$update_ports, {
g6_update_ports(
g6_proxy("graph"),
c("A", "B"),
list(
A = list(remove = c("out1", "out2")),
B = list(
add = list(g6_port(key = "new", label = "new", placement = "top")),
update = list(g6_port(key = "in2", label = "Updated label"))
)
)
)
})
}
shinyApp(ui, server)
You can also get ports with g6_get_ports(), or specifically
g6_get_input_ports() and g6_get_output_ports().
Use case: interactive network builder.
The example below, an interactive network builder where you can append new nodes by clicking a port guide, summarizes these features. The new node is created at the mouse position and connected to the clicked port.

library(shiny)
library(g6R)
options(
"g6R.mode" = "dev",
"g6R.layout_on_data_change" = TRUE
)
ui <- fluidPage(
g6_output("dag"),
verbatimTextOutput("clicked_port")
)
server <- function(input, output, session) {
output$dag <- render_g6(
g6(
nodes = g6_nodes(
g6_node(
id = 1,
type = "custom-circle-node",
style = list(
labelText = "Node 1"
),
ports = g6_ports(
g6_input_port(
key = "input-1",
placement = "left"
),
g6_output_port(
key = "output-1",
placement = "right"
),
g6_input_port(
key = "input-12",
placement = "top"
)
)
),
g6_node(
id = 2,
type = "custom-circle-node",
style = list(
labelText = "Node 2"
),
ports = g6_ports(
g6_input_port(
key = "input-2",
placement = "left"
),
g6_output_port(
key = "output-2",
placement = "right"
)
)
)
),
edges = g6_edges(
g6_edge(
source = 1,
target = 2,
style = list(
sourcePort = "output-1",
targetPort = "input-2",
endArrow = TRUE,
startArrow = FALSE,
endArrowType = "vee"
)
)
)
) |>
g6_layout() |>
g6_options(
animation = FALSE,
edge = list(style = list(endArrow = TRUE))
) |>
g6_behaviors(
click_select(multiple = TRUE),
drag_element(
enable = JS(
"(e) => {
return !e.shiftKey && !e.altKey;
}"
)
),
drag_canvas(
enable = JS(
"(e) => {
return e.targetType === 'canvas' && !e.shiftKey && !e.altKey;
}"
)
),
zoom_canvas(),
create_edge(
enable = JS(
"(e) => {
return e.shiftKey}"
)
)
) |>
# Allow to dynamically remove an edge
g6_plugins(
context_menu(
enable = JS("(e) => e.targetType === 'edge'"),
getItems = JS(
"() => {
return [
{ name: 'Remove edge', value: 'remove_edge' }
];
}"
),
onClick = JS(
"(value, target, current) => {
const graph = HTMLWidgets
.find(`#${target.closest('.g6').id}`)
.getWidget();
console.log(current.id);
if (current.id === undefined) return;
if (value === 'remove_edge') {
graph.removeEdgeData([current.id]);
graph.draw();
}
}
"
)
)
)
)
output$clicked_port <- renderPrint({
input[["dag-selected_port"]]
})
proxy <- g6_proxy("dag")
# Add a new node when a port is clicked from the guide
# at the mouse position (close to the guide)
observeEvent(input[["dag-selected_port"]], {
new_id <- 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, # avoids overlapping with the guide.
y = pos$y,
labelText = paste("Node", new_id)
),
ports = g6_ports(
g6_port(
key = sprintf("input-%s", new_id),
type = "input",
placement = "left",
fill = "#52C41A",
r = 4
),
g6_port(
key = sprintf("output-%s", new_id),
type = "output",
placement = "right",
fill = "#FF4D4F",
r = 4
)
)
)) |>
g6_add_edges(
g6_edge(
source = input[["dag-selected_port"]][["node"]],
target = new_id,
style = list(
sourcePort = input[["dag-selected_port"]][["port"]],
targetPort = sprintf("input-%s", new_id),
endArrow = TRUE
)
)
)
})
}
shinyApp(ui, server)
Conclusion
Ports bring a new level of precision and flexibility to workflow building in applications like blockr.dag. By allowing you to define exactly how data flows between nodes, and to control the number and type of connections, ports make it much easier to design complex analytical pipelines. Whether you’re joining tables, binding rows, or using other data transformations, ports help keep your workflows organized. We’re excited to see how you use these new capabilities!
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 blockr.dag or g6R repositories.