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)
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:
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.
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:
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)
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)
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.
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…
…but they are displayed when you look at the SVG in Firefox:
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…
…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:
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",
" • Slightly indented",
" † Dagger",
" Δ Delta",
"  - Emspace and a hyphen",
sep = "\\\\l"
)