2

I often create Sankey-diagrams in R via {sankeyD3}, because it seems to be the package with the most options/features to do so. However, one feature that is missing is the ability to set the order of nodes on the y-axis (although this issue tried to fix that?).

Therefore, I must arrange the nodes manually afterwards. I can do this by setting dragY = TRUE when creating the diagram and then exporting it to an html file via htmlwidgets::saveWidget(). This allows me to manually drage the nodes when opening the html file.

reprex

# remotes::install_github("fbreitwieser/sankeyD3")
links <- data.frame(
  source = c(0, 0, 0, 1, 2, 3, 4, 4),
  target = c(1, 2, 3, 4, 4, 4, 5, 6),
  value = c(2, 3, 4, 2, 3 , 4, 4, 5)
)

nodes <- data.frame(
  label = c("A1", "B1", "B3", "B2", "C1", "D1", "D2"),
  yOrder = c(1, 1, 3, 2, 1, 1, 2)
)

out <- sankeyD3::sankeyNetwork(
  Links = links,
  Nodes = nodes,
  Source = "source",
  Target = "target",
  Value  = "value",
  NodeID = "label",
  fontFamily = "Arial",
  fontSize = 12,
  numberFormat = ",.1s",
  height = 500,
  width = 700,
  dragY = TRUE)

htmlwidgets::saveWidget(out,
                        file = here::here("out.html"),
                        selfcontained = TRUE)

and here is a screenshot showing the exported html on the left and the one where I manually rearranged the nodes on the right: enter image description here

Question

My goal is to insert the edited diagram into a word-document in the best possible quality. So I guess I want to know how to export the edited html-file to a SVG format or similar?

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
Paul Schmidt
  • 1,072
  • 10
  • 23
  • I've used webshot to save htmlWidgets as image files in the past. Not sure if it will do exactly what you want but it might be worth looking up. – olorcain Jun 20 '22 at 10:27
  • 1
    You can get a PDF with the **webshot** package or the **webshot2** package. – Stéphane Laurent Jun 20 '22 at 10:36
  • 1
    For SVG there is **WebVector**: http://cssbox.sourceforge.net/webvector/ – Stéphane Laurent Jun 20 '22 at 10:38
  • @moodymudskipper' as Stephane comments above, `webshot2` can output to pdf: `webshot2::webshot("out.html", "out.pdf")` (with caveats on browser requirements) – user20650 Jun 22 '23 at 16:24
  • AFAICT the pdf output doesn't contain vector graphics so it doesn't work for me – moodymudskipper Jun 22 '23 at 17:19
  • ah okay.@moodymudskipper. For info, how do you tell ... the pdf looks okay under zoom to me – user20650 Jun 22 '23 at 17:56
  • I don't know for sure tbh. It's what I understand from how webshot2 works, I didn't see the doc mention vector graphics. – moodymudskipper Jun 23 '23 at 06:36
  • yes, I thought chromium may have a facility for some fancy screenshot type stuff but couldn't see anything in the docs (and possibly wishing for a unicorn) . But the resulting pdf didnt get pixelated under 1000% zoom. – user20650 Jun 23 '23 at 13:05

2 Answers2

2

You can use a headless browser to do this - example below use chromote:

Chromote is an R implementation of the Chrome DevTools Protocol. It works with Chrome, Chromium, Opera, Vivaldi, and other browsers based on Chromium. By default it uses Google Chrome (which must already be installed on the system). To use a different browser, see Specifying which browser to use.

So, if you have Chrome this will code will open the output of saveWidget and run it in a headless Chrome session. The steps are: create a new session; navigate to the htmlWidgets::saveWidget output; wait till the load event has fired (async); then identify the svg element and get the outerHTML; then persist this:

outFile <- 'C:\\Users\\robin\\Downloads\\out.html'
svg1File <- 'C:\\Users\\robin\\Downloads\\out1.svg'
svg2File <- 'C:\\Users\\robin\\Downloads\\out2.svg'

b <- chromote::ChromoteSession$new()
b$Page$navigate(outFile, wait_ = FALSE)
b$Page$loadEventFired(wait_ = FALSE)$
  then(function(value) {
    svgList <- b$Runtime$evaluate('document.querySelector("svg").outerHTML')
  })$
  then(function (value) {
    writeLines(value$result$value, svg1File)
  })

There is a little problem in this which you can see by double clicking on this out1.svg file and note it does not render the image. This is because the xmlns tag is missing from the svg element. You can add this with functions from the xml2 package like so:

svg <- xml2::read_xml(svg1File)
root <- xml2::xml_find_first(svg, xpath = '//svg')
xml2::xml_set_attr(root, attr = 'xmlns', value = 'http://www.w3.org/2000/svg')
xml2::write_xml(svg, svg2File)

This adds the standard namespace attribute to the svg and this file will render in a browser. I have an old version of Word that does not import svg so cannot test that.

Full code based on yours:

outFile <- 'C:\\Users\\robin\\Downloads\\out.html'
svg1File <- 'C:\\Users\\robin\\Downloads\\out1.svg'
svg2File <- 'C:\\Users\\robin\\Downloads\\out2.svg'

links <- data.frame(
  source = c(0, 0, 0, 1, 2, 3, 4, 4),
  target = c(1, 2, 3, 4, 4, 4, 5, 6),
  value = c(2, 3, 4, 2, 3 , 4, 4, 5)
)

nodes <- data.frame(
  label = c("A1", "B1", "B3", "B2", "C1", "D1", "D2"),
  yOrder = c(1, 1, 2, 3, 1, 1, 2)
)

out <- sankeyD3::sankeyNetwork(
  Links = links,
  Nodes = nodes,
  Source = "source",
  Target = "target",
  Value  = "value",
  NodeID = "label",
  fontFamily = "Arial",
  fontSize = 12,
  numberFormat = ",.1s",
  height = 500,
  width = 700,
  dragY = TRUE)


htmlwidgets::saveWidget(out,
                        file = outFile,
                        selfcontained = TRUE)

b <- chromote::ChromoteSession$new()
b$Page$navigate(outFile, wait_ = FALSE)
b$Page$loadEventFired(wait_ = FALSE)$
  then(function(value) {
    svgList <- b$Runtime$evaluate('document.querySelector("svg").outerHTML')
  })$
  then(function (value) {
    writeLines(value$result$value, svg1File)
  })

svg <- xml2::read_xml(svg1File)
root <- xml2::xml_find_first(svg, xpath = '//svg')
xml2::xml_set_attr(root, attr = 'xmlns', value = 'http://www.w3.org/2000/svg')
xml2::write_xml(svg, svg2File)
Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56
  • Thanks, this looks good but unfortunately I can't reproduce this on macOS, I use tempfile() to create correct path at the start of the script, then I use `b$Page$navigate(paste0("file://", normalizePath(outFile)), wait_ = FALSE)` because the default macOS path seems not to be supported (It says the URL is invalid), but then it chokes on the next step, svg1File is not created – moodymudskipper Jun 28 '23 at 13:57
  • I could make it work by running separately `svgList <- b$Runtime$evaluate('document.querySelector("svg").outerHTML')` then `writeLines(svgList$result$value, svg1File)` instead of the chained `b$Page$loadEventFired(...)$...` call. Thanks a lot. – moodymudskipper Jun 28 '23 at 14:11
1

Open the result in a browser, make any manual adjustments you want, then use an SVG extractor like https://nytimes.github.io/svg-crowbar/ to save it as an SVG.

CJ Yetman
  • 8,373
  • 2
  • 24
  • 56