Adding custom graph attributes

85 views Asked by At

I am trying to add a custom graph attribute to an igraph object. I use the code

graph = set_graph_attr(graph, "directed", FALSE)
data_json <- d3_igraph(igraph)
write(data_json, "graph.json")

and the output appears as

{
  "nodes" [
     {
        "name": "something"
     },
     {
        "name": "something_else"
     }
  ],
  "links": [
      {
         "source": "something",
         "target": "something_else"
      }
   ],
  "attributes": {
      "directed": false
  }
}

The added attribute goes into an attributes section of the json, but what I would really like to output is:

{
  "nodes" [
     {
        "name": "something"
     },
     {
        "name": "something_else"
     }
  ],
  "links": [
      {
         "source": "something",
         "target": "something_else"
      }
   ],
  "attributes": {
  },
  "directed": false
}

where the new attribute is added at the top level of the json rather than in the attributes section. Is that possible? Eventually the plan is to add several custom attributes including metadata with a more complex structure.

2

There are 2 answers

2
Martin Morgan On BEST ANSWER

The example isn't fully reproducible, but the development version of the CRAN package rjsoncons has j_patch_apply(), which allows one to 'patch' a JSON document as illustrated at the JSON patch web site. So after

remotes::install_github(mtmorgan/rjsoncons)

one should be able to do something like

json <- d3_igraph(igraph)
patch <- '[{"op": "move", "from": "/attributes/directed", "path": "/directed"}]'
json_patched <- j_patch_apply(json, patch)
0
SamR On

The source for d3r::d3_igraph() hard codes attributes into their own section of the json on line 47. So it doesn't seem possible to do it using that function. However, the approach of d3r is to convert the graph to a data frame, rename the columns to the keys d3.js expects and then send it off to jsonlite. This can be replicated fairly easily with the additional feature of putting attributes at the top level.

generate_d3_json <- function(g, numeric_index = FALSE, attrs_to_keep = "directed") {
    # Convert to data frame like d3r
    d <- igraph::as_data_frame(g, what = "both")
    d$vertices$id <- rownames(d$vertices)

    # zero-indexing so we can compare output to d3r
    if (numeric_index) {
        d <- lapply(d, \(dat) {
            nm <- intersect(c("from", "to", "id"), names(dat))
            dat[nm] <- lapply(dat[nm], \(x) as.character(as.integer(x) - 1))
            dat
        })
    }

    l <- c(
        list(
            nodes = d$vertices,
            links = setNames(d$edges, c("source", "target", tail(names(d$edges), -2)))
        ),
        igraph::graph_attr(g)[attrs_to_keep]
    )
    jsonlite::toJSON(l, auto_unbox = TRUE, pretty = TRUE)
}

You can choose which top-level attributes to keep with the attrs_to_keep parameter. If your vertices are identified by numbers then set numeric_index to TRUE. I've set the default to FALSE as in your example they're named. The function will return a json in the desired format.

Graphs with named vertices

g <- igraphdata::Koenigsberg |>
    igraph::set_graph_attr("directed", FALSE)

generate_d3_json(g)

Output:

{
  "nodes": [
    {
      "name": "Altstadt-Loebenicht",
      "Euler_letter": "B",
      "id": "Altstadt-Loebenicht",
      "_row": "Altstadt-Loebenicht"
    },
    {
      "name": "Kneiphof",
      "Euler_letter": "A",
      "id": "Kneiphof",
      "_row": "Kneiphof"
    },
    {
      "name": "Vorstadt-Haberberg",
      "Euler_letter": "C",
      "id": "Vorstadt-Haberberg",
      "_row": "Vorstadt-Haberberg"
    },
    {
      "name": "Lomse",
      "Euler_letter": "D",
      "id": "Lomse",
      "_row": "Lomse"
    }
  ],
  "links": [
    {
      "source": "Altstadt-Loebenicht",
      "target": "Kneiphof",
      "Euler_letter": "a",
      "name": "Kraemer Bruecke"
    },
    {
      "source": "Altstadt-Loebenicht",
      "target": "Kneiphof",
      "Euler_letter": "b",
      "name": "Schmiedebruecke"
    },
    {
      "source": "Altstadt-Loebenicht",
      "target": "Lomse",
      "Euler_letter": "f",
      "name": "Holzbruecke"
    },
    {
      "source": "Kneiphof",
      "target": "Lomse",
      "Euler_letter": "e",
      "name": "Honigbruecke"
    },
    {
      "source": "Vorstadt-Haberberg",
      "target": "Lomse",
      "Euler_letter": "g",
      "name": "Hohe Bruecke"
    },
    {
      "source": "Kneiphof",
      "target": "Vorstadt-Haberberg",
      "Euler_letter": "c",
      "name": "Gruene Bruecke"
    },
    {
      "source": "Kneiphof",
      "target": "Vorstadt-Haberberg",
      "Euler_letter": "d",
      "name": "Koettelbruecke"
    }
  ],
  "directed": false
}

We can check this worked:

d3r_output <- jsonlite::fromJSON(d3r::d3_igraph(g))
my_d3_output <- jsonlite::fromJSON(generate_d3_json(g))

common_names <- intersect(names(d3r_output), names(my_d3_output))
# ^^ c("nodes", "links")

identical(
    d3r_output[common_names],
    my_d3_output[common_names]
)
# [1] TRUE

Graphs without named nodes

If you have nodes without names then set numeric_index = TRUE. This is not actually necessary but it will zero-index meaning we can easily compare with the d3r output:

g <- igraph::make_ring(3) |>
    igraph::set_graph_attr("directed", FALSE)

generate_d3_json(g, numeric_index = TRUE)
# ^^ produces the json output

Again, we can check that this gives the same output as d3r:

d3r_output <- jsonlite::fromJSON(d3r::d3_igraph(g))
my_d3_output <- jsonlite::fromJSON(generate_d3_json(g, numeric_index = TRUE))
identical(d3r_output[common_names], my_d3_output[common_names]) # TRUE