Mermaid with R + consort

Visualization
R
OJS
Published

January 9, 2024

Code
#lapply(c('tidyverse','data.table','igraph','ggraph','kableExtra'),library,character.only=TRUE))
pacman::p_load(tidyverse,data.table,igraph,ggraph,kableExtra,DiagrammeR,png,consort,data.table) 

flowchart LR
    data1(Varied Training<br/>800-1000<br/>1000-1200<br/>1200-1400)
    data2(Constant Training<br/>800-1000)
    Test1(Testing - No Feedback<br/>100-300<br/>350-550<br/>600-800)
    Test2(Test From Train<br/>800-1000<br/>1000-1200<br/>1200-1400)
    Test3(Testing - Feedback<br/>100-300<br/>350-550<br/>600-800)

    data1 --> Test1
    data2 --> Test1
    Test1 --> Test2
    Test2 --> Test3
    
%% https://quarto.org/docs/authoring/diagrams.html

Mean Vx over training blocks

data1 Varied Training800-10001000-12001200-1400Test1 Testing - No Feedback100-300350-550600-800data1->Test1 data2 Constant Training800-1000data2->Test1 Test2 Test From Train800-10001000-12001200-1400Test1->Test2 Test3 Testing - Feedback100-300350-550600-800Test2->Test3
Figure 1: This is a simple graphviz graph.
data1 Varied Training800-10001000-12001200-1400Test1 Testing - No Feedback100-300350-550600-800data1->Test1 data2 Constant Training800-1000data2->Test1 Test2 Test From Train800-10001000-12001200-1400Test1->Test2 Test3 Testing - Feedback100-300350-550600-800Test2->Test3
Figure 2: This is a simple graphviz graph.

subgraph

cluster_counterbalanced Counterbalanced Orderdata1 Varied Training800-10001000-12001200-1400subgraph1 subgraph1data1->subgraph1 data2 Constant Training800-1000data2->subgraph1 Test1 Testing - No Feedback100-300350-550600-800Test2 Test From Train800-10001000-12001200-1400Test3 Testing - Feedback100-300350-550600-800subgraph1->Test3

cluster Counterbalanced Orderdata1 Varied Training800-10001000-12001200-1400Test1 Testing - No Feedback100-300350-550600-800data1->Test1 data2 Constant Training800-1000data2->Test1 Test3 Testing - Feedback100-300350-550600-800Test2 Test From Train800-10001000-12001200-1400Test1->Test2 Test2->Test3

cluster Counterbalanced Orderdata1 Varied Training800-10001000-12001200-1400Test1 Testing - No Feedback100-300350-550600-800data1->Test1 data2 Constant Training800-1000data2->Test1 Test3 Testing - Feedback100-300350-550600-800Test2 Test From Train800-10001000-12001200-1400Test1->Test2 Test2->Test3

ALM Input1 Input1Input2 Input2Output1 Output1Input1->Output1 w_{11}Output2 Output2Input1->Output2 w_{12}Output3 Output3Input1->Output3 w_{13}Input2->Output1 w_{21}Input2->Output2 w_{22}Input2->Output3 w_{23}equation1 a_i(X) = e^{-γ(X-X_i)^2}, normalized: a_i(X) / Σa_i(X)equation2 O_j(X) = Σ w_{ji} ⋅ a_i(X), P[Y_j | X] = O_j(X) / Σ O_k(X)

test

ALM cluster_input Input Layercluster_output Output LayerInput1 Input1Output1 Output1Input1->Output1 w_11Output2 Output2Input1->Output2 w_12Output3 Output3Input1->Output3 w_13Input2 Input2Input2->Output1 w_21Input2->Output2 w_22Input2->Output3 w_23equation1 Input activation ai(X) = exp(-gamma*(X-Xi)^2), normalized by dividing by sumequation2 Output Oj(X) = sum(w_ji * ai(X)), Probability P[Yj | X] = Oj(X) / sum(Ok(X))

test2

G E1 z zE2 x100 x100a ab bx1 x1a->x1 x2 x2a->x2 a->x100 b->x1 b->x2 b->x100 z->x1 z->x2 z->x100 Y Yx1->Y x2->Y x100->Y

test3

G clusterX n1n2n1->n2 n3n1->n3 n4 n2->n4 n5 n4->n5 n6 n5->n6 J1J2 J1->J2 J6 J1->J6 J4J5 J5->J4 J6->J5

test4

Nest Cluster - GraphViz

D cluster_p Parentcluster_c2 Child twocluster_c1 Child onecluster_gc_1 Grand-Child onecluster_gc_2 Grand-Child twoa ab bc cd de e

G E1 E1z zE2 x100 x100a ab bx1 x1a->x1 x2 x2a->x2 a->x100 b->x1 b->x2 b->x100 z->x1 z->x2 z->x100 Y Yx1->Y x2->Y x100->Y

test5

G n1n2n1->n2 n4n2->n4

test6

ALM Input1 X1Input2 X2Output1 Y1Input1:e->Output1:w w11Output2 Y2Input1:e->Output2:w w12Output3 Y3Input1:e->Output3:w w13Input2:e->Output1:w w21Input2:e->Output2:w w22Input2:e->Output3:w w23label_input Input Layerlabel_output Output Layer

test7

G cluster_0 layer 1 (input layer)cluster_1 layer 2 (hidden layer)cluster_2 layer 3 (hidden layer)cluster_3 layer 4 (output layer)x0 x0a02 a0(2)x0:e->a02:wa12 a5(2)x0:e->a12:w a22 a1(2)x0:e->a22:w a32 a2(2)x0:e->a32:w a42 a3(2)x0:e->a42:w a52 a4(2)x0:e->a52:w x1 x1x1:e->a12:w x1:e->a22:w x1:e->a32:w x1:e->a42:w x1:e->a52:w x2 x2x2:e->a12:w x2:e->a22:w x2:e->a32:w x2:e->a42:w x2:e->a52:w x3 x3x3:e->a12:w x3:e->a22:w x3:e->a32:w x3:e->a42:w x3:e->a52:w a03 a0(3)a02:e->a03:wa13 a3(3)a02:e->a13:w a23 a4(3)a02:e->a23:w a33 a5(3)a02:e->a33:w a43 a1(3)a02:e->a43:w a53 a2(3)a02:e->a53:w a12:e->a13:w a12:e->a23:w a12:e->a33:w a12:e->a43:w a12:e->a53:w a22:e->a13:w a22:e->a23:w a22:e->a33:w a22:e->a43:w a22:e->a53:w a32:e->a13:w a32:e->a23:w a32:e->a33:w a32:e->a43:w a32:e->a53:w a42:e->a13:w a42:e->a23:w a42:e->a33:w a42:e->a43:w a42:e->a53:w a52:e->a13:w a52:e->a23:w a52:e->a33:w a52:e->a43:w a52:e->a53:w O1 a1(4)a03:e->O1:w O2 a2(4)a03:e->O2:w O3 a3(4)a03:e->O3:w O4 a4(4)a03:e->O4:w a13:e->O1:w a13:e->O2:w a13:e->O3:w a13:e->O4:w a23:e->O1:w a23:e->O2:w a23:e->O3:w a23:e->O4:w a33:e->O1:w a33:e->O2:w a33:e->O3:w a33:e->O4:w a43:e->O1:w a43:e->O2:w a43:e->O3:w a43:e->O4:w a53:e->O1:w a53:e->O2:w a53:e->O3:w a53:e->O4:w

test8

G cluster_0 layer 1 (Input layer)cluster_1 layer 2 (hidden layer)cluster_2 layer 3 (output layer)x1 a12 x1->a12 a22 x1->a22 a32 x1->a32 x2 x2->a12 x2->a22 x2->a32 x3 x3->a12 x3->a22 x3->a32 O a12->O a22->O a32->O

G x0 x0x1 x1a12 a1(2)x0:e->a12:w a22 a2(2)x0:e->a22:w a32 a3(2)x0:e->a32:w a42 a4(2)x0:e->a42:w a52 a5(2)x0:e->a52:w a02 a0(2)a03 a0(3)a13 a1(3)a02:e->a13:w a23 a2(3)a02:e->a23:w a33 a3(3)a02:e->a33:w a43 a4(3)a02:e->a43:w a53 a5(3)a02:e->a53:w O1 a1(4)a03:e->O1:w O2 a2(4)a03:e->O2:w O3 a3(4)a03:e->O3:w O4 a4(4)a03:e->O4:w x2 x2x1:e->a12:w x1:e->a22:w x1:e->a32:w x1:e->a42:w x1:e->a52:w x3 x3x2:e->a12:w x2:e->a22:w x2:e->a32:w x2:e->a42:w x2:e->a52:w x3:e->a12:w x3:e->a22:w x3:e->a32:w x3:e->a42:w x3:e->a52:w a12:e->a13:w a12:e->a23:w a12:e->a33:w a12:e->a43:w a12:e->a53:w a22:e->a13:w a22:e->a23:w a22:e->a33:w a22:e->a43:w a22:e->a53:w a32:e->a13:w a32:e->a23:w a32:e->a33:w a32:e->a43:w a32:e->a53:w a42:e->a13:w a42:e->a23:w a42:e->a33:w a42:e->a43:w a42:e->a53:w a52:e->a13:w a52:e->a23:w a52:e->a33:w a52:e->a43:w a52:e->a53:w a13:e->O1:w a13:e->O2:w a13:e->O3:w a13:e->O4:w a23:e->O1:w a23:e->O2:w a23:e->O3:w a23:e->O4:w a33:e->O1:w a33:e->O2:w a33:e->O3:w a33:e->O4:w a43:e->O1:w a43:e->O2:w a43:e->O3:w a43:e->O4:w a53:e->O1:w a53:e->O2:w a53:e->O3:w a53:e->O4:w l0 layer 1 (input layer)l1 layer 2 (hidden layer)l2 layer 3 (hidden layer)l3 layer 4 (output layer)

threevar k ?X1 X1k->X1 n@_{S}X2 X2k->X2 θ@^{(h)}X3 X3k->X3 ?3z1 d1z1->X1 z2 d2z2->X2 z3 d3z3->X3

Code
grViz('digraph threevar {
      rankdir=LR;
      size="8,4";
      node [fontsize=14 shape=box];
      edge [fontsize=10];
      center=1;
      {rank=min k }
      {rank=same X1 X2 X3 }
      {rank=max z1 z2 z3 }
      z1 [shape=circle label="d1"];
      z2 [shape=circle label="d2"];
      z3 [shape=circle label="d3"];
      k [label="?" shape="ellipse"];
      k -> X1 [label="n@_{S}"];
      k -> X2 [label="&theta;@^{(h)}"];
      k -> X3 [label="?3"];
      z1 -> X1;
      z2 -> X2;
      z3 -> X3;
    }
      ')
Code
grViz("digraph dot {
      graph [style = filled, fillcolor = white]
      
      node [shape = circle,
            style = filled, fillcolor = white,
            fixedsize = true, width = 0.5, height = 0.5,
            fontname = 'Times-italic']
        c
        d
      node [shape = doublecircle,
            style = filled, fillcolor = white,
            fixedsize = true, width = 0.5, height = 0.5,
            fontname = 'Times-italic']
        thetah [label = '&theta;@^{(h)}']
        thetaf [label = '&theta;@^{(f)}']
      node [shape = square,
            style = filled, fillcolor = grey,
            fixedsize = true, width=0.5, height=0.5,
            fontname = 'Times-italic']
        h
        f
        ns [label = 'n@_{S}']
        nn [label = 'n@_{N}']
      
      edge [color = black]
        c -> thetah -> h
        d -> thetaf -> f
        c -> thetaf
        d -> thetah
        ns -> h
        nn -> f
      {rank = max; ns; nn}
      }")
Code
grViz("digraph dot {
      graph [style = filled, fillcolor = white,
             rankdir = LR,
             newrank = true]
      
      node [shape = circle,
            style = filled, fillcolor = white,
            fixedsize = true, width = 0.5, height = 0.5,
            fontname = 'Times-italic']
        p [fillcolor = gray]
        gamma [label = '&gamma;']
        omega [label = '&omega;', shape = doublecircle]
        beta [label = '&beta;']
      subgraph cluster_out{
        fontsize = 8
        label = <<I>j</I>:試行>
        labelloc = b
        labeljust = r
        subgraph cluster_in{
          fontsize = 8
          label = <<I>k</I>:回数>
          labelloc = b
          labeljust = r
          thetajk [label = '&theta;@_{jk}', shape = doublecircle]
          djk [label = 'd@_{jk}', shape = square, fillcolor = gray]
        }}
      edge [color=black]
        p -> omega -> thetajk -> djk
        gamma -> omega
        beta -> thetajk 
      {rank = same; gamma; omega}
      {rank = same; beta; thetajk}
      }")
Code
grViz('
digraph G {
    fontname="Helvetica,Arial,sans-serif"
    node [fontname="Helvetica,Arial,sans-serif"]
    edge [fontname="Helvetica,Arial,sans-serif"]

    subgraph cluster_0 {
        style=filled;
        color=lightgrey;
        node [style=filled,color=white];
        a0 -> a1 -> a2 -> a3;
        label = "process #1";
    }

    subgraph cluster_1 {
        node [style=filled];
        b0 -> b1 -> b2 -> b3;
        label = "process #2";
        color=blue
    }
    start -> a0;
    start -> b0;
    a1 -> b3;
    b2 -> a3;
    a3 -> a0;
    a3 -> end;
    b3 -> end;

    start [shape=Mdiamond];
    end [shape=Msquare];
}')
Code
#https://graphviz.org/Gallery/directed/neural-network.html

grViz('digraph G {
  fontname="Helvetica,Arial,sans-serif"
  node [fontname="Helvetica,Arial,sans-serif"]
  edge [fontname="Helvetica,Arial,sans-serif"]
  concentrate=True;
  rankdir=TB;
  node [shape=record];
  140087530674552 [label="title: InputLayer\n|{input:|output:}|{{[(?, ?)]}|{[(?, ?)]}}"];
  140087537895856 [label="body: InputLayer\n|{input:|output:}|{{[(?, ?)]}|{[(?, ?)]}}"];
  140087531105640 [label="embedding_2: Embedding\n|{input:|output:}|{{(?, ?)}|{(?, ?, 64)}}"];
  140087530711024 [label="embedding_3: Embedding\n|{input:|output:}|{{(?, ?)}|{(?, ?, 64)}}"];
  140087537980360 [label="lstm_2: LSTM\n|{input:|output:}|{{(?, ?, 64)}|{(?, 128)}}"];
  140087531256464 [label="lstm_3: LSTM\n|{input:|output:}|{{(?, ?, 64)}|{(?, 32)}}"];
  140087531106200 [label="tags: InputLayer\n|{input:|output:}|{{[(?, 12)]}|{[(?, 12)]}}"];
  140087530348048 [label="concatenate_1: Concatenate\n|{input:|output:}|{{[(?, 128), (?, 32), (?, 12)]}|{(?, 172)}}"];
  140087530347992 [label="priority: Dense\n|{input:|output:}|{{(?, 172)}|{(?, 1)}}"];
  140087530711304 [label="department: Dense\n|{input:|output:}|{{(?, 172)}|{(?, 4)}}"];
  140087530674552 -> 140087531105640;
  140087537895856 -> 140087530711024;
  140087531105640 -> 140087537980360;
  140087530711024 -> 140087531256464;
  140087537980360 -> 140087530348048;
  140087531256464 -> 140087530348048;
  140087531106200 -> 140087530348048;
  140087530348048 -> 140087530347992;
  140087530348048 -> 140087530711304;
}')
Code
require(data.table)
require(qreport)
require(consort)

# Load the necessary libraries
require(data.table)
require(qreport)

# Define the mermaid diagram
x <- '
graph LR
  InputLayer[Input Layer] --> I1[{{I1}}]
  InputLayer --> I2[{{I2}}]
  InputLayer --> I3[{{I3}}]
  I1 --> O1[w1]
  I1 --> O2[w2]
  I1 --> O3[w3]
  I1 --> O4[w4]
  I2 --> O1[w5]
  I2 --> O2[w6]
  I2 --> O3[w7]
  I2 --> O4[w8]
  I3 --> O1[w9]
  I3 --> O2[w10]
  I3 --> O3[w11]
  I3 --> O4[w12]
  OutputLayer[Output Layer] --> O1
  OutputLayer --> O2
  OutputLayer --> O3
  OutputLayer --> O4
  OutputLayer --> ALM[ALM Response]
  OutputLayer --> EXAM[EXAM Response]
  ALM --> EXAM[Pure ALM Model]
  EXAM --> ALM[ALM with EXAM Response Component]
'

# Generate the mermaid diagram
makemermaid(x,
            I1 = '![Gaussian Curve](gaussian_curve_input_node_1.png)',
            I2 = '![Gaussian Curve](gaussian_curve_input_node_2.png)',
            I3 = '![Gaussian Curve](gaussian_curve_input_node_3.png)',
            file = 'assets/alm_exam.mer'
            )

# Output the mermaid diagram
# cat('```{mermaid}\n')
# cat(readLines('assets/alm_exam.mer'), sep = '\n')
# cat('```\n')
graph LR
  InputLayer[Input Layer] --> I1[![Gaussian Curve](gaussian_curve_input_node_1.png)]
  InputLayer --> I2[![Gaussian Curve](gaussian_curve_input_node_2.png)]
  InputLayer --> I3[![Gaussian Curve](gaussian_curve_input_node_3.png)]
  I1 --> O1[w1]
  I1 --> O2[w2]
  I1 --> O3[w3]
  I1 --> O4[w4]
  I2 --> O1[w5]
  I2 --> O2[w6]
  I2 --> O3[w7]
  I2 --> O4[w8]
  I3 --> O1[w9]
  I3 --> O2[w10]
  I3 --> O3[w11]
  I3 --> O4[w12]
  OutputLayer[Output Layer] --> O1
  OutputLayer --> O2
  OutputLayer --> O3
  OutputLayer --> O4
  OutputLayer --> ALM[ALM Response]
  OutputLayer --> EXAM[EXAM Response]
  ALM --> EXAM[Pure ALM Model]
  EXAM --> ALM[ALM with EXAM Response Component]
Figure 3: ALM diagram produced with mermaid with individual exclusions linked to the overall exclusions node, and with a tooltip to show more detail
Code
library(consort)
set.seed(1001)
N <- 300

trialno <- sample(c(1000:2000), N)
exc <- rep(NA, N)
exc[sample(1:N, 15)] <- sample(c("Sample not collected", "MRI not collected", "Other"),
                                15, replace = T, prob = c(0.4, 0.4, 0.2))

arm <- rep(NA, N)
arm[is.na(exc)] <- sample(c("Conc", "Seq"), sum(is.na(exc)), replace = T)

fow1 <- rep(NA, N)
fow1[!is.na(arm)] <- sample(c("Withdraw", "Discontinued", "Death", "Other", NA),
                            sum(!is.na(arm)), replace = T, 
                            prob = c(0.05, 0.05, 0.05, 0.05, 0.8))
fow2 <- rep(NA, N)
fow2[!is.na(arm) & is.na(fow1)] <- sample(c("Protocol deviation", "Outcome missing", NA),
                                          sum(!is.na(arm) & is.na(fow1)), replace = T, 
                                          prob = c(0.05, 0.05, 0.9))
df <- data.frame(trialno, exc, arm, fow1, fow2)
out <- consort_plot(data = df,
             order = c(trialno = "Population",
                          exc    = "Excluded",
                          arm     = "Randomized patient",
                          fow1    = "Lost of Follow-up",
                          trialno = "Finished Followup",
                          fow2    = "Not evaluable",
                          trialno = "Final Analysis"),
             side_box = c("exc", "fow1", "fow2"),
             allocation = "arm",
             labels = c("1" = "Screening", "2" = "Randomization",
                        "5" = "Final"),
             cex = 0.6)

plot(out)

Code
plot(out, grViz = TRUE)
Code
require(Hmisc)
require(data.table)
require(qreport)
hookaddcap()
N <- 1000
set.seed(1)
r <- data.table(
  id    = 1 : N,
  age   = round(rnorm(N, 60, 15)),
  pain  = sample(0 : 5, N, replace=TRUE),
  hxmed = sample(0 : 1, N, replace=TRUE, prob=c(0.95, 0.05))   )
# Set consent status to those not excluded at screening
r[age >= 40 & pain > 0 & hxmed == 0,
  consent := sample(0 : 1, .N, replace=TRUE, prob=c(0.1, 0.9))]
# Set randomization status for those consenting
r[consent == 1,
  randomized := sample(0 : 1, .N, replace=TRUE, prob=c(0.15, 0.85))]
# Add treatment and follow-up time to randomized subjects
r[randomized == 1, tx     := sample(c('A', 'B'), .N, replace=TRUE)]
r[randomized == 1, futime := pmin(runif(.N, 0, 10), 3)]
# Add outcome status for those followed 3 years
# Make a few of those followed 3 years missing
r[futime == 3,
  y := sample(c(0, 1, NA), .N, replace=TRUE, prob=c(0.75, 0.2, 0.05))]
# Print first 15 subjects
kabl(r[1 : 15, ])
id age pain hxmed consent randomized tx futime y
1 51 2 1 NA NA NA NA NA
2 63 2 0 1 1 A 3.0000 0
3 47 1 0 1 1 A 3.0000 NA
4 84 5 0 1 0 NA NA NA
5 65 3 0 1 1 B 3.0000 1
6 48 4 0 1 1 A 3.0000 0
7 67 3 0 1 1 B 2.0566 NA
8 71 0 0 NA NA NA NA NA
9 69 5 0 1 1 B 1.2815 NA
10 55 2 0 1 1 B 1.2388 NA
11 83 4 0 1 1 A 3.0000 1
12 66 5 0 1 1 A 3.0000 0
13 51 1 0 1 1 B 3.0000 0
14 27 2 0 NA NA NA NA NA
15 77 2 0 1 1 A 3.0000 0
Code
r[, exc := seqFreq('pain-free'  = pain  == 0,
                    'Hx med'    = hxmed == 1,
                                  age < 40,
                    noneNA=TRUE)]
eo  <- attr(r[, exc], 'obs.per.numcond')
mult <- paste0('1, 2, ≥3 exclusions: n=',
                eo[2], ', ',
                eo[3], ', ',
                eo[-(1:3)]  )

r[, .q(qual, consent, fin) :=
    .(is.na(exc),
      ifelse(consent == 1, 1, NA),
      ifelse(futime  >= 3, 1, NA))]
            
require(consort)
# consort_plot used to take a coords=c(0.4, 0.6) argument that prevented
# the collision you see here
consort_plot(r,
             orders = c(id      = 'Screened',
                        exc     = 'Excluded',
                        qual    = 'Qualified for Randomization',
                        consent = 'Consented',
                        tx      = 'Randomized',
                        fin     = 'Finished',
                        y       = 'Outcome\nassessed'),
             side_box = 'exc',
             allocation = 'tx',
             labels=c('1'='Screening', '3'='Consent', '4'='Randomization', '6'='Follow-up'))

htab

Code
h <- function(n, label) paste0(label, ' (n=', n, ')')
htab <- function(x, label=NULL, split=! length(label), br='\n') {
  tab <- table(x)
  w <- if(length(label)) paste0(h(sum(tab), label), ':', br)
  f <- if(split) h(tab, names(tab)) 
  else
    paste(paste0('   ', h(tab, names(tab))), collapse=br)
  if(split) return(f)
  paste(w, f, sep=if(length(label))'' else br)
}  
count <- function(x, by=rep(1, length(x)))
  tapply(x, by, sum, na.rm=TRUE)

w <- r[, {
 g <-
   add_box(txt=h(nrow(r),       'Screened'))                    |>
   add_side_box(htab(exc,       'Excluded'))                    |>
   add_box(h(count(is.na(exc)), 'Qualified for Randomization')) |>
   add_box(h(count(consent),    'Consented'))                   |>
   add_box(h(count(randomized), 'Randomized'))                  |>
   add_split(htab(tx))                                          |>
   add_box(h(count(fin, tx),    'Finished'))                    |>
   add_box(h(count(! is.na(y), tx), 'Outcome\nassessed'))       |>
   add_label_box(c('1'='Screening',     '3'='Consent',
                   '4'='Randomization', '6'='Follow-up'))
 plot(g)
}
]

mermaid maker

Code
addCap('fig-doverview-mermaid1', 'Consort diagram produced by `mermaid`')
x <- 'flowchart TD
  S["Screened (n={{N0}})"] --> E["{{excl}}"]
  S   --> Q["Qualified for Randomization (n={{Nq}})"]
  Q   --> C["Consented (n={{Nc}})"]
  C   --> R["Randomized (n={{Nr}})"]
  R   --> TxA["A (n={{Ntxa}})"]
  R   --> TxB["B (n={{Ntxb}})"]
  TxA --> FA["Finished (n={{Ntxaf}})"]
  TxB --> FB["Finished (n={{Ntxbf}})"]
  FA  --> OA["Outcome assessed (n={{Ntxao}})"]
  FB  --> OB["Outcome assessed (n={{Ntxbo}})"]
classDef largert fill:lightgray,width:1.5in,height:10em,text-align:right,font-size:0.8em;
class E largert;
'

w <- r[, 
makemermaid(x,
            N0   = nrow(r),
            excl = htab(exc, 'Excluded', br='<br>'),
            Nq   = count(is.na(exc)),
            Nc   = count(consent),
            Nr   = count(randomized),
            Ntxa = count(tx == 'A'),
            Ntxb = count(tx == 'B'),
            Ntxaf= count(tx == 'A' & fin),
            Ntxbf= count(tx == 'B' & fin),
            Ntxao= count(tx == 'A' & ! is.na(y)),
            Ntxbo= count(tx == 'B' & ! is.na(y)),
            file = 'assets/mermaid1.mer'
            )
]
flowchart TD
  S["Screened (n=1000)"] --> E["Excluded (n=286):<br>   pain-free (n=156)<br>   age < 40 (n=85)<br>   Hx med (n=45)"]
  S   --> Q["Qualified for Randomization (n=714)"]
  Q   --> C["Consented (n=634)"]
  C   --> R["Randomized (n=534)"]
  R   --> TxA["A (n=285)"]
  R   --> TxB["B (n=249)"]
  TxA --> FA["Finished (n=204)"]
  TxB --> FB["Finished (n=175)"]
  FA  --> OA["Outcome assessed (n=196)"]
  FB  --> OB["Outcome assessed (n=165)"]
classDef largert fill:lightgray,width:1.5in,height:10em,text-align:right,font-size:0.8em;
class E largert;
Figure 4: Consort diagram produced by mermaid

node plot

Code
# Create some service functions so later it will be easy to change from
# mermaid to graphviz
makenode       <- function(name, label) paste0(name, '["', label, '"]')
makeconnection <- function(from, to)    paste0(from, ' --> ', to)

exclnodes <- function(x, from='E', root='E', seq=FALSE, remain=FALSE) {
  # Create complete node specifications for individual exclusions, each
  # linking to overall exclusion count assumed to be in node root.
  # Set seq=TRUE to make use of the fact that the exclusions were
  # done in frequency priority order so that each exclusion is in
  # addition to the previous one.  Leave seq=FALSE to make all exclusions
  # subservient to root.  Use remain=TRUE to include # obs remaining
    # remain=TRUE assumes noneNA specified to seqFreq
  tab <- table(x)
  i <- 1 : length(tab)
    rem <- if(remain) paste0(', ', length(x) - cumsum(tab), ' remain')
  labels <- paste0(names(tab), ' (n=', tab, rem, ')')
  nodes  <- if(seq) makenode(ifelse(i == 1, paste0(root, '1'), paste0(root, i)),
                             labels)
            else    makenode(paste0(root, i), labels)
  connects <- if(seq) makeconnection(ifelse(i == 1, from, paste0(root, i - 1)),
                                     paste0(root, i))
              else makeconnection(from, paste0(root, i))
   paste(c(nodes, connects), collapse='\n')
}

# Create parallel treatment nodes
# Treatments are assumed to be in order by the tx variable
# and will appear left to right in the diagram
# Treatment node names correspond to that and are Tx1, Tx2, ...
# root: root of new nodes, from: single node name to connect from
# fromparallel: root of connected-from node name which is to be
# expanded by adding the integers 1, 2, ... number of treatments.

Txs <- r[, if(is.factor(tx)) levels(tx) else sort(unique(tx))]

parNodes <- function(counts, root, from=NULL, fromparallel=NULL,
                      label=Txs) {
  if(! identical(names(counts), Txs)) stop('Txs not consistent')
  k <- length(Txs)
  ns <- paste0(' (n=', counts, ')')
   nodenames <- paste0(root, 1 : k)
  nodes <- makenode(nodenames, paste0(label, ns))
  connects <- if(length(fromparallel)) makeconnection(paste0(fromparallel, 1 : k), nodenames)
              else                     makeconnection(from,                        nodenames)
  paste(c(nodes, connects), collapse='\n')
    }

# Create tooltip text from tabulation created by seqFreq earlier
efreq <- data.frame('# Exclusions'= (1 : length(eo)) - 1,
                    '# Subjects'  = eo, check.names=FALSE)
efreq <- subset(efreq, `# Subjects` > 0)
# Convert to text which will be wrapped by the html
excltab <- paste(capture.output(print(efreq, row.names=FALSE)),
                 collapse='\n')


addCap('fig-doverview-mermaid2', 'Consort diagram produced with `mermaid` with individual exclusions linked to the overall exclusions node, and with a tooltip to show more detail')

x <- '
flowchart TD
  S["Screened (n={{N0}})"] --> E["Excluded (n={{Ne}})"]
  {{exclsep}}
  E1 & E2 & E3 --> M["{{mult}}"]
  S   --> Q["Qualified for Randomization (n={{Nq}})"]
  Q   --> C["Consented (n={{Nc}})"]
  C   --> R["Randomized (n={{Nr}})"]
  {{txcounts}}
  {{finished}}
  {{outcome}}
click E callback "{{excltab}}"
'

w <- r[, 
makemermaid(x,
  N0       = nrow(r),
  Ne       = count(! is.na(exc)),
  exclsep  = exclnodes(exc),  # add seq=TRUE to put exclusions vertical
  excltab  = excltab,         # tooltip text
  mult     = mult,  # separate node: count multiple exclusions
  Nq       = count(is.na(exc)),
  Nc       = count(consent),
  Nr       = count(randomized),
  txcounts = parNodes(table(tx),         'Tx', from='R'),
  finished = parNodes(count(fin, by=tx), 'F',  fromparallel='Tx',
                      label='Finished'),
  outcome  = parNodes(count(! is.na(y), by=tx), 'O',
                      fromparallel='F', label='Outcome assessed'),
  file='mermaid2.mer'  # save generated code for another use
)
]


makenode       <- function(name, label) paste0(name, ' [label="', label, '"];')
makeconnection <- function(from, to)    paste0(from, ' -> ', to, ';')

# Create data frame from tabulation created by seqFreq earlier
efreq <- data.frame('# Exclusions'= (1 : length(eo)) - 1,
                    '# Subjects'  = eo, check.names=FALSE)
efreq <- subset(efreq, `# Subjects` > 0)

x <- 'digraph {
  graph [pad="0.5", nodesep="0.5", ranksep="2", splines=ortho]
  //  splines=ortho for square connections
  node  [shape=box, fontsize="30"]
  rankdir=TD;
  S [label="Screened (n={{N0}})"];
  E [label="Excluded (n={{Ne}})"];
  S -> E;
  {{exclsep}}
  M [label="{{mult}}"];
  E1 -> M;
  E2 -> M;
  E3 -> M;
  Q [label="Qualified for Randomization (n={{Nq}})"];
  C [label="Consented (n={{Nc}})"];
  R [label="Randomized (n={{Nr}})"];
  S -> Q;
  Q -> C;
  C -> R;
  {{txcounts}}
  {{finished}}
  {{outcome}}
  efreq [label=<{{efreq}}>];
  M -> efreq [dir=none, style=dotted];
}
'

w <- r[, 
makegraphviz(x,
  N0       = nrow(r),
  Ne       = count(! is.na(exc)),
  exclsep  = exclnodes(exc),  # add seq=TRUE to put exclusions vertical
  efreq    = efreq,
  mult     = mult,  # separate node: count multiple exclusions
  Nq       = count(is.na(exc)),
  Nc       = count(consent),
  Nr       = count(randomized),
  txcounts = parNodes(table(tx),         'Tx', from='R'),
  finished = parNodes(count(fin, by=tx), 'F',  fromparallel='Tx',
                      label='Finished'),
  outcome  = parNodes(count(! is.na(y), by=tx), 'O',
                      fromparallel='F', label='Outcome assessed'),
  file='graphviz.dot'
)
]
#  addCap('fig-doverview-graphviza', 'Consort diagram produced with `graphviz` with detailed exclusion frequencies in a separate node', scap='Consort diagram produced with `graphviz`')
flowchart TD
  S["Screened (n=1000)"] --> E["Excluded (n=286)"]
  E1["pain-free (n=156)"]
E2["age < 40 (n=85)"]
E3["Hx med (n=45)"]
E --> E1
E --> E2
E --> E3
  E1 & E2 & E3 --> M["1, 2, ≥3 exclusions: n=260, 25, 1"]
  S   --> Q["Qualified for Randomization (n=714)"]
  Q   --> C["Consented (n=634)"]
  C   --> R["Randomized (n=534)"]
  Tx1["A (n=285)"]
Tx2["B (n=249)"]
R --> Tx1
R --> Tx2
  F1["Finished (n=204)"]
F2["Finished (n=175)"]
Tx1 --> F1
Tx2 --> F2
  O1["Outcome assessed (n=196)"]
O2["Outcome assessed (n=165)"]
F1 --> O1
F2 --> O2
click E callback " # Exclusions # Subjects
            0        714
            1        260
            2         25
            3          1"
Figure 5: Consort diagram produced with mermaid with individual exclusions linked to the overall exclusions node, and with a tooltip to show more detail
Code
getHdata(support)
setDT(support)
# addCap('fig-doverview-missflow', 'Flowchart of sequential exclusion of observations due to missing values')
vars <-  .q(age, sex, dzgroup, edu, income, meanbp, wblc,
            alb, bili, crea, glucose, bun, urine)
ex <- missChk(support, use=vars, type='seq') # seq: don't make report

# Create tooltip text from tabulation created by seqFreq
oc   <- attr(ex, 'obs.per.numcond')
freq <- data.frame('# Exclusions'= (1 : length(oc)) - 1,
                   '# Subjects'  = oc, check.names=FALSE)
freq <- subset(freq, `# Subjects` > 0)

x <- '
digraph {
  graph [pad="0.5", nodesep="0.5", ranksep="2", splines=ortho]
  //  splines=ortho for square connections
  node  [shape=box, fontsize="30"]
  rankdir=TD;
  Enr [label="Enrolled (n={{N0}})"];
  Enr;
  {{exclsep}}
    Extab [label=<{{excltab}}>];
  Enr:e -> Extab [dir=none];
}
'
makegraphviz(x,
  N0        = nrow(support),
  exclsep   = exclnodes(ex, from='Enr', seq=TRUE, remain=TRUE),
  excltab   = freq,
  file      = 'support.dot'
)
Code
grViz('
graph G {
fontname="Helvetica,Arial,sans-serif"
node [fontname="Helvetica,Arial,sans-serif"]
edge [fontname="Helvetica,Arial,sans-serif"]
I5 [shape=ellipse,color=red,style=bold,label="Caroline Bouvier Kennedy\nb. 27.11.1957 New York",image="images/165px-Caroline_Kennedy.jpg",labelloc=b];
I1 [shape=box,color=blue,style=bold,label="John Fitzgerald Kennedy\nb. 29.5.1917 Brookline\nd. 22.11.1963 Dallas",image="images/kennedyface.jpg",labelloc=b];
I6 [shape=box,color=blue,style=bold,label="John Fitzgerald Kennedy\nb. 25.11.1960 Washington\nd. 16.7.1999 over the Atlantic Ocean, near Aquinnah, MA, USA",image="images/180px-JFKJr2.jpg",labelloc=b];
I7 [shape=box,color=blue,style=bold,label="Patrick Bouvier Kennedy\nb. 7.8.1963\nd. 9.8.1963"];
I2 [shape=ellipse,color=red,style=bold,label="Jaqueline Lee Bouvier\nb. 28.7.1929 Southampton\nd. 19.5.1994 New York City",image="images/jacqueline-kennedy-onassis.jpg",labelloc=b];
I8 [shape=box,color=blue,style=bold,label="Joseph Patrick Kennedy\nb. 6.9.1888 East Boston\nd. 16.11.1969 Hyannis Port",image="images/1025901671.jpg",labelloc=b];
I10 [shape=box,color=blue,style=bold,label="Joseph Patrick Kennedy Jr\nb. 1915\nd. 1944"];
I11 [shape=ellipse,color=red,style=bold,label="Rosemary Kennedy\nb. 13.9.1918\nd. 7.1.2005",image="images/rosemary.jpg",labelloc=b];
I12 [shape=ellipse,color=red,style=bold,label="Kathleen Kennedy\nb. 1920\nd. 1948"];
I13 [shape=ellipse,color=red,style=bold,label="Eunice Mary Kennedy\nb. 10.7.1921 Brookline"];
I9 [shape=ellipse,color=red,style=bold,label="Rose Elizabeth Fitzgerald\nb. 22.7.1890 Boston\nd. 22.1.1995 Hyannis Port",image="images/Rose_kennedy.JPG",labelloc=b];
I15 [shape=box,color=blue,style=bold,label="Aristotle Onassis"];
I3 [shape=box,color=blue,style=bold,label="John Vernou Bouvier III\nb. 1891\nd. 1957",image="images/BE037819.jpg",labelloc=b];
I4 [shape=ellipse,color=red,style=bold,label="Janet Norton Lee\nb. 2.10.1877\nd. 3.1.1968",image="images/n48862003257_1275276_1366.jpg",labelloc=b];
 I1 -- I5  [style=bold,color=blue]; 
 I1 -- I6  [style=bold,color=orange]; 
 I2 -- I6  [style=bold,color=orange]; 
 I1 -- I7  [style=bold,color=orange]; 
 I2 -- I7  [style=bold,color=orange]; 
 I1 -- I2  [style=bold,color=violet]; 
 I8 -- I1  [style=bold,color=blue]; 
 I8 -- I10  [style=bold,color=orange]; 
 I9 -- I10  [style=bold,color=orange]; 
 I8 -- I11  [style=bold,color=orange]; 
 I9 -- I11  [style=bold,color=orange]; 
 I8 -- I12  [style=bold,color=orange]; 
 I9 -- I12  [style=bold,color=orange]; 
 I8 -- I13  [style=bold,color=orange]; 
 I9 -- I13  [style=bold,color=orange]; 
 I8 -- I9  [style=bold,color=violet]; 
 I9 -- I1  [style=bold,color=red]; 
 I2 -- I5  [style=bold,color=red]; 
 I2 -- I15  [style=bold,color=violet]; 
 I3 -- I2  [style=bold,color=blue]; 
 I3 -- I4  [style=bold,color=violet]; 
 I4 -- I2  [style=bold,color=red]; 
}')