TL;DR or The End Link to heading

# RStudio 2024.04.2+764 "Chocolate Cosmos" Release 
# (e4392fc9ddc21961fd1d0efd47484b43f07a4177, 2024-06-05) for Ubuntu Jammy
# Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) 
# rstudio/2024.04.2+764 Chrome/120.0.6099.291 Electron/28.3.1 Safari/537.36, 
# Quarto 1.4.555

library(DiagrammeR)
library(DiagrammeRsvg)
library(tidyverse)
library(rsvg)
library(xml2)

# The `str_c` function just lets me combine several lines of text 
# and specify a separator
# Useful if you want to generate a list.
text <- str_c(
  "Very long, long, long line of text",
  "    Slightly indented",
  sep = "\\\\l"
)

graph <-
  "digraph{
    graph [layout = dot splines = ortho]
    node [shape = rectangle width = 4]

    A [nojustify=true label = 'A very very very long line\\l    Another Line\\l' ]
    B [nojustify=true label = '@@1\\l']

    A -> B
    } 
  
    [1]: text
    "

grviz_output <- grViz(graph)

grviz_export <- export_svg(grviz_output)

# There are a few ways to export grViz htmlwidgets, but we are going to
# edit the XML before we export the file
grviz_read_xml <- read_xml(grviz_export)

# This selects all of the "text" nodes in the XML
nodes <- xml_find_all(grviz_read_xml, "//*[name()='text']")

# We need to add 'xml:space="Preserve"' to each of the text nodes otherwise
# any leading spaces (i.e. indentation) will be stripped away
xml_attr(nodes, "xml:space") <- "preserve"

# TIL that an SVG file is just XML!
write_xml(grviz_read_xml, "xml.svg")

rsvg_png('xml.svg', file = 'xml.png', width = 600)

A flow chart showing two boxes, one on top of the other. In each box, the second line is slightly indented relative to the first line.

The story Link to heading

Follow along if you want an exciting story of how I added indented multi-line labels to a flow chart node on DiagrammeR. The protagonists of this story include the ’escape sequence’ that escapes several times, different interpretations of the SVG specification, and modifying XML. It is a story so deep that it took me a whole day to unravel and write up.

Background Link to heading

The story starts with me wanting to create a flow chart using DiagrammeR where the node labels span multiple lines and some lines are indented relative to the first line. Something like this:

A flow chart showing two boxes, one on top of the other. In each box, the second line is slightly indented relative to the first line.

The idea was that the subsequent lines could be a list.

Software used Link to heading

Software Version
Debian Testing (updated 2024-08-01)
r 4.4.1
R Studio 2024.04.2
DiagrammeR 1.0.11
DiagrammeRsvg 0.1
tidyverse 2.0.0
rsvg 2.6.0
xml2 1.3.6

Step 1 - Adding new lines Link to heading

library(DiagrammeR)
library(DiagrammeRsvg)

graph <-

  "digraph{

  graph [layout = dot splines = ortho]
  node [shape = rectangle width = 4]

  A [nojustify=true label = 'Very long first line with lots of text \n Shorter 2nd line \n 3rd line']
  B [nojustify=true label = 'Text']

  A -> B
}"

grviz_output <- grViz(graph)

export_svg(grviz_output) |> charToRaw() |> rsvg_svg("initial.svg")

rsvg_png('initial.svg', file = 'initial.png', width = 600)

The \n escape code inserts a new line, which is what we want, but the text remains centered in the node.

Flow chart with two text boxes arranged vertically. There are three lines of text in the top box, all centered

Step 2 - Left justifying text Link to heading

<Enter the first protagonist - the ’escape sequence’ that escapes several times>

According to the graphviz documentation we can use \l to create a new line that is left-justified. But when we change \n to \l

  A [label = 'Very long first line with lots of text \l Shorter 2nd line \l 3rd line']

..we get this error:

Error: '\l' is an unrecognized escape in character string (<input>:8:55)

It turns out \l is not an r escape sequence. To fix this, we need to escape the escape sequence with an extra \ (i.e. change \l to \\l)

  A [label = 'Very long first line with lots of text \\l Shorter 2nd line \\l 3rd line']

But this only justifies the first and second line:

Flow chart with two text boxes arranged vertically. The first two lines in the top box are left justified, the third line is centered

We need to add an \\l to each line that we want justified

library(DiagrammeR)
library(DiagrammeRsvg)

graph <-

  "digraph{

  graph [layout = dot splines = ortho]
  node [shape = rectangle width = 4]

  A [label = 'Very long first line with lots of text \\l Shorter 2nd line \\l 3rd line \\l']
  B [label = 'Text']

  A -> B
}"

grviz_output <- grViz(graph)

export_svg(grviz_output) |> charToRaw() |> rsvg_svg("step2.svg")

rsvg_png('step2.svg', file = 'step2.png', width = 600)

Flow chart with two text boxes arranged vertically. All three lines in the top box are left justified

Step 3 - Centering the first line Link to heading

Let’s get the first line back into the centre of the box. We do this by adding nojustify=true to the node:

library(DiagrammeR)
library(DiagrammeRsvg)

graph <-

  "digraph{

  graph [layout = dot splines = ortho]
  node [shape = rectangle width = 4]

  A [nojustify=true label = 'Very long first line with lots of text \\l Shorter 2nd line \\l 3rd line \\l']
  B [label = 'Text']

  A -> B
}"

grviz_output <- grViz(graph)

export_svg(grviz_output) |> charToRaw() |> rsvg_svg("step3.svg")

rsvg_png('step3.svg', file = 'step3.png', width = 600)

Flow chart with two text boxes arranged vertically. The first line in the top box is centered. The second and third lines in the first box are left justified to be aligned with the start of the first line

Step 4, attempt 1 - Indenting subsequent lines Link to heading

Let’s see what happens when I try to indent the second and third lines by adding extra spaces:

  A [nojustify=true label = 'Very long first line with lots of text \\l        Shorter 2nd line \\l      3rd line \\l']

Nothing.

Flow chart with two text boxes arranged vertically. The first line in the top box is centered. The second and third lines in the first box are left justified to be aligned with the start of the first line

It is not possible to see what is going on inside this SVG file because charToRaw and rsvg_svg generate this kind of output:

<path fill-rule="nonzero" fill="rgb(100%, 100%, 100%)" fill-opacity="1" d="M 0.429688 138 L 0.429688 0 L 295.570312 0 L 295.570312 138 Z M 0.429688 138 "/>
<path fill="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(0%, 0%, 0%)" stroke-opacity="1" stroke-miterlimit="4" d="M 287.998205 -130.599949 L 0.0017953 -130.599949 L 0.0017953 -71.797189 L 287.998205 -71.797189 Z M 287.998205 -130.599949 " transform="matrix(0.99711, 0, 0, 0.99711, 4.416179, 134.011561)"/>

Not useful.

Interlude - Changing the way SVGs are saved Link to heading

<Enter the second protagonist - different interpretations of the SVG specification>

Today I learned that SVG files are just XML. That means we can write the generated XML straight to a file and see what is inside. To do this, we add the xml2 library and change export_svg(grviz_output) |> charToRaw() |> rsvg_svg("step4.svg") to export_svg(grviz_output) |> read_xml() |> write_xml("step4.svg")

library(DiagrammeR)
library(DiagrammeRsvg)
library(xml2)

graph <-

  "digraph{

  graph [layout = dot splines = ortho]
  node [shape = rectangle width = 4]

  A [nojustify=true label = 'Very long first line with lots of text \\l      Shorter 2nd line \\l     3rd line \\l']
  B [label = 'Text']

  A -> B
}"

grviz_output <- grViz(graph)

#export_svg(grviz_output) |> charToRaw() |> rsvg_svg("step4.svg")

export_svg(grviz_output) |> read_xml() |> write_xml("step4.svg")

rsvg_png('step4.svg', file = 'step4.png', width = 600)

When we look inside the SVG file, we can see that the spaces are retained:

<text text-anchor="start" x="42.9074" y="-113.8" font-family="Times,serif" font-size="14.00" fill="#000000">Very long first line with lots of text </text>
<text text-anchor="start" x="42.9074" y="-97" font-family="Times,serif" font-size="14.00" fill="#000000">      Shorter 2nd line </text>

The spaces are not kept when the SVG is converted to a PNG…

Flow chart with two text boxes arranged vertically. The first line in the top box is centered. The second and third lines in the first box are left justified to be aligned with the start of the first line

…but they are displayed when you look at the SVG in Firefox:

Output showing a Firefox screenshot. The first line in the top box is centered. The second and third lines in the top box are left justified and aligned with the start of the first line, but they are indented by several spaces

The SVG specification says that extra spaces, including leading and trailing spaces, are meant to be removed. Firefox was the only program that retained the leading spaces when I looked at the SVG file. Every other program removed them.

Step 4, attempt 2 - Modifying XML Link to heading

<Enter the third protagonist - modifying XML>

Recall that SVG is just XML:

<text text-anchor="start" x="42.9074" y="-97" font-family="Times,serif" font-size="14.00" fill="#000000">      Shorter 2nd line </text>

Missing from this XML is an attribute that tells the program to retain the spaces: xml:space="preserve"

<text text-anchor="start" x="42.9074" y="-97" font-family="Times,serif" font-size="14.00" fill="#000000" xml:space="preserve">      Shorter 2nd line </text>

We add this attribute by modifying the XML before we write the SVG file:

library(DiagrammeR)
library(DiagrammeRsvg)
library(xml2)

graph <-

  "digraph{

  graph [layout = dot splines = ortho]
  node [shape = rectangle width = 4]

  A [nojustify=true label = 'Very long first line with lots of text \\l      Shorter 2nd line \\l     3rd line \\l']
  B [label = 'Text']

  A -> B
}"

grviz_output <- grViz(graph)

grviz_export <- export_svg(grviz_output)

grviz_read_xml <- read_xml(grviz_export)

nodes <- xml_find_all(grviz_read_xml, "//*[name()='text']")

xml_attr(nodes, "xml:space") <- "preserve"

write_xml(grviz_read_xml, "step4.svg")

rsvg_png('step4.svg', file = 'step4.png', width = 600)

And…

Flow chart with two text boxes arranged vertically. The first line in the top box is centered. The second and third lines in the first box are left justified to be aligned with the start of the first line but they are indented by a few spaces

…just like magic.

Step 5 - Making it easier to write multi-line text Link to heading

<Re-enter our first protagonist - the ’escape sequence’ that escapes several times>

It is a little hard to write multi-line text with indentation spaces all on one line of r code. We can make this easier by storing the text and substituting it into the flow chart.

We use the str_c function from tidyverse to write the multi-line text. Each line is written separately with spaces for indentations. We then need to tell str_c to use \\\\l to separate each section of text.

Why \\\\l? The text that we are putting together is processed twice. This means that we need to escape the \l escape sequence twice. The first time the text is processed, the stored_text is passed to the graph statement and the \\\\l is converted to \\l. The second time the text is processed, to generate the graph label, the \\l is converted to \l.

Don’t forget to put a \\l after the substitution in the label (i.e. the @@1).

library(DiagrammeR)
library(DiagrammeRsvg)
library(tidyverse)
library(xml2)

stored_text <- str_c(
  "Very long, long, long line of text",
  "    Slightly indented",
  sep = "\\\\l"
)

graph <-

  "digraph{

  graph [layout = dot splines = ortho]
  node [shape = rectangle width = 4]

  A [nojustify=true label = 'Very long first line with lots of text \\l      Shorter 2nd line \\l     3rd line \\l']
  B [nojustify=true label = '@@1\\l']

  A -> B
}

[1]: stored_text
"

grviz_output <- grViz(graph)

grviz_export <- export_svg(grviz_output)

grviz_read_xml <- read_xml(grviz_export)

nodes <- xml_find_all(grviz_read_xml, "//*[name()='text']")

xml_attr(nodes, "xml:space") <- "preserve"

write_xml(grviz_read_xml, "xml.svg")

rsvg_png('xml.svg', file = 'xml.png', width = 600)

And we get:

Flow chart with two text boxes arranged vertically. The first line in the top box is centered. The second and third lines in the first box are left justified to be aligned with the start of the first line but they are indented by a few spaces

Going further Link to heading

As a little congratulations for not stopping at the TL;DR section, here’s how you add custom unicode characters:

text <- str_c(
  "Very long, long, long line of text",
  "    &#8226; Slightly indented",
  "    &#8224; Dagger",
  "    &#916; Delta",
  "&#8195; - Emspace and a hyphen",
  sep = "\\\\l"
)

Flow chart with two text boxes arranged vertically. The first line in the top box is centered. The second and third lines in the first box are left justified to be aligned with the start of the first line but they are indented by a few spaces. The second box now has more text. The first line in the second box is centered. All other lines in the second box are indented. The second line starts with a bullet point. The third line starts with a unicode dagger. The fourth line starts with a unicode delta. The fifth line starts with a unicode emspace and then a hyphen.