cynkra


g6R: 0.5.0 CRAN update.

From the Blog
Shiny
R

Author

David Granjon

Published

Hex Sticker of the R package g6R. It shows a colorful database icon.

Introduction

g6R continues to evolve as the go-to solution for interactive network visualization in R, especially for Shiny applications. With g6R 0.5.0, we focused on improving robustness, flexibility, and user experience. This release introduces new features and API improvements for data validation.

To quickly get started, install it with:

# CRAN
install.packages("g6R")
# With pak: https://pak.r-lib.org/
pak::pak("g6R")
# Latest GitHub main
pak::pak("cynkra/g6R")

Main changes

Data validation and helper functions

g6R now validates input data more strictly. We added g6_nodes(), g6_edges() and g6_combos() helpers for creating their respective graph elements. We suggest to use these helper functions instead of passing lists or dataframes to g6 as they provide additional safety checks. For convenience and to support usecases we may not have thought about, we exported coercion functions for all elements such as as_g6_nodes() or as_g6_data(). We use them internally to maintain backward compatibility while ensuring better data validation. See below an example of the various ways to create nodes:

# Create a single node
node <- g6_node(id = "A", type = "circle", style = list(fill = "#FFB6C1"))

# Create multiple nodes from a data frame
df <- data.frame(id = c("A", "B"), type = c("circle", "rect"))
nodes <- as_g6_nodes(df)

# With g6_nodes()
nodes <- g6_nodes(
  g6_node(id = "A", type = "circle"),
  g6_node(id = "B", type = "rect")
)

# with a list
lst <- list(
  list(id = "A", type = "circle"),
  list(id = "B", type = "rect")
)
nodes <- as_g6_nodes(lst)
#> [[1]]
#> $id
#> [1] "A"
#> 
#> $type
#> [1] "circle"
#> 
#> attr(,"class")
#> [1] "g6_node"    "g6_element"
#> 
#> [[2]]
#> $id
#> [1] "B"
#> 
#> $type
#> [1] "rect"
#> 
#> attr(,"class")
#> [1] "g6_node"    "g6_element"
#> 
#> attr(,"class")
#> [1] "g6_nodes"

In g6R, the recommended way to assemble graph data is by using the helper functions g6_data() and as_g6_data():

# Create nodes and edges
my_nodes <- data.frame(id = c("A", "B"))
my_edges <- data.frame(source = "A", target = "B")

# Assemble graph data using g6_data()
graph <- g6_data(
  nodes = my_nodes,
  edges = my_edges
)
graph
#> $nodes
#> [[1]]
#> $id
#> [1] "A"
#> 
#> attr(,"class")
#> [1] "g6_node"    "g6_element"
#> 
#> [[2]]
#> $id
#> [1] "B"
#> 
#> attr(,"class")
#> [1] "g6_node"    "g6_element"
#> 
#> attr(,"class")
#> [1] "g6_nodes"
#> 
#> $edges
#> [[1]]
#> $source
#> [1] "A"
#> 
#> $target
#> [1] "B"
#> 
#> $id
#> [1] "A-B"
#> 
#> attr(,"class")
#> [1] "g6_edge"    "g6_element"
#> 
#> attr(,"class")
#> [1] "g6_edges"
#> 
#> attr(,"class")
#> [1] "g6_data"

# Or use as_g6_data() directly with a list
lst <- list(
  nodes = list(
    list(id = "A"),
    list(id = "B")
  ),
  edges = list(
    list(source = "A", target = "B")
  )
)
graph2 <- as_g6_data(lst)

all.equal(graph, graph2)
#> [1] TRUE

Existing g6R code is unlikely to break as backwards compatibility was ensured during the development of this release. However, passing invalid parameters will now error instead of sending broken data to JavaScript, and we believe this is for the best!

Support for svg rendering

By default, G6 renders elements on the canvas, which means graph elements are not part of the DOM. It’s been a bit of a blocker for us for headless testing with shinytest2 because we could not targert or trigger specific actions. Now, in g6_options() you can select an SVG renderer like so:

g6_options(renderer = JS("() => new SVGRenderer()"))

This way, all graph elements are part of the DOM and can be targeted with shinytest2.

Layout updates

In some cases, particularly with the antv_dagre_layout() any node addition or removal may rearrange the graph in unexpected ways, which was confusing for users. Now, most proxy function like g6_add_nodes() won’t recalculate the layout by default. You can still manually call g6_update_layout() or set some options:

# Revert to the old behavior
options("g6R.layout_on_data_change" = TRUE)

As a side note g6R.preserve_elements_position option can be used in combination with g6R.layout_on_data_change so that existing nodes actually keep their old position when adding new data. Be aware that this may not work well with combos and only if g6_options(animation = FALSE). We are currently investigating better ways to handle this.

Shiny updates

Capture mouse position

We exposed a new Shiny input, that is input[["<graph_ID>-mouse_position"]] which can serve to add nodes at the current mouse position. The value is a list with x and y coordinates. Utilised with the create_edge() behavior, you can now implement drag-and-drop node creation as shown in the example below:

library(shiny)
library(g6R)
library(bslib)

nodes <- data.frame(id = 1)

ui <- page_fluid(
  g6_output("graph")
)

server <- function(input, output, session) {
  output$graph <- render_g6({
    g6(nodes = nodes) |>
      g6_layout() |>
      g6_behaviors(
        create_edge(
          enable = JS("
            (e) => { return e.shiftKey; }"
          ),
          onFinish = JS(
            "(edge) => {
              const graph = HTMLWidgets.find('#graph').getWidget();
              const targetType = graph.getElementType(edge.target);
              if (targetType !== 'node') {
                graph.removeEdgeData([edge.id]);
              } else {
                Shiny.setInputValue('added_edge', edge);
              }
            }"
          )
        )
      )
  })

  next_id <- reactiveVal(2)

  observeEvent(input$added_edge, {
    edge <- input$added_edge
    pos <- input[["graph-mouse_position"]]
    # Only add node if released on canvas
    if (edge$targetType == "canvas") {
      # Add new node at drop position
      g6_proxy("graph") |>
      g6_add_nodes(
        g6_node(
          id = next_id(), 
          style = list(x = pos$x, y = pos$y)
        )
      )
      # Connect source node to new node
      g6_proxy("graph") |>
      g6_add_edges(
        g6_edge(source = edge$source, target = next_id())
      )
      next_id(next_id() + 1)
    }
  })
}

shinyApp(ui, server)

The above is possible owing to a modification in create_edge(). By default, it was not possible to release the edge drawer on the canvas, but now, we allow this through input$added_edge$targetType.

Selection detection

Elements selected via brush_select() or lasso_select() have a custom input handler, which helps to differentiate between different modes of selection.:

input[["<graph_ID>-selected_combo"]]
[1] "1" "2"
attr(,"eventType")
[1] "brush_select"

Context menu

We expose input$<graph_ID>-contextmenu which contains information about the element right-clicked by the user:

library(shiny)
library(g6R)
library(bslib)

nodes <- data.frame(id = c("node1", "node2"))
edges <- data.frame(source = "node1", target = "node2")

ui <- page_fluid(
  g6_output("graph"),
  verbatimTextOutput("contextmenu_info")
)

server <- function(input, output, session) {
  output$graph <- render_g6({
    g6(
      nodes = nodes,
      edges = edges
    ) |>
      g6_layout() |>
      g6_plugins(
        context_menu()
      )
  })

  output$contextmenu_info <- renderPrint({
    input[["graph-contextmenu"]]
  })
}

shinyApp(ui, server)

Prod and dev modes

We added options("g6R.mode) which can be set to "dev" or "prod". In dev mode, g6R will display any JS error in a Shiny notification. This may be helpful for you to debug any data issue and also for us while testing the package.

Bug fixes and minor improvements

We fixed a bunch of issues in the select behaviors (click, brush). You can review the full list here

Next steps

If you have any feedback, you can reach out to us via the contact page on this site. Alternatively, please report any issues or feature requests on the GitHub repository.