Mermaid.js Dynamic Render

In this coding notebook I hope to accomplish:

  • Step 1: Simple rendering of a simple static mermaid diagram

  • Step 2: Figure out how to "pass" data between cells (this is necessary for generation of data in anything other than JavaScript, since each cell can only execute one language, save for a couple exceptions such as HTML+CSS+JavaScript, SQLite+Python, etc.)

    • Method 2A: Save data to the browser window local storage

    • Method 2B: Save data to a Nextjournal (or NJ notebook) specific place

    • Method 2C: Other methods ?

    • Skill 2D: Save data so you can pass it between cells via JavaScript

    • Skill 2E: Save data so you can pass it between cells via ClojureScript

  • Step 3: Render a programmatically (dynamically) generated mermaid diagram

--- Step 1 ---

Import the Mermaid.js library via JavaScript require function

mermaidJs = require('mermaid@8.7/dist/mermaid.min.js')

Create a Nextjournal "viewof" rendering container for the mermaid diagram to be rendered in

viewof mermaidContainer = html`<div id="mermaid" />`

Create a function which will render a mermaid diagram into a div tag with the ID "mermaid".

mermaid = function (...values) {
  const div = (viewof mermaidContainer); //document.getElementById('mermaid');
  if (!div) {
    throw 'Cannot find div with id "mermaid"';
  }
  
  const src = String.raw(...values).trim();
  const id = "mmd" + Math.round(Math.random() * 10000);
  div.append(html`<div id="${id}" />`);
  
  try {
    const result = mermaidJs.render(id, src, undefined);
    return svg([result]);
  } catch (ex) {
    console.error(ex);
    throw ex;
  }
}

Render a simple diagram as SVG using the "mermaid" function*

Question: Is this usage of mermaid below as a function, or is it something else? Typically functions must be called with opening and closing parentheses to take parameters...

// Example using the Observable Runtime, taken from <https://observablehq.com/@oogetyboogety/mermaid-js>.
mermaid`
  stateDiagram-v2
    [*] --> Still
    Still --> [*]
    Still --> Moving
    Moving --> Still
    Moving --> Crash
    Crash --> [*]
`
// temporarily turning these off from being rendered
mermaid`
  graph TD
    A[Christmas] -->|Get money| B(Go shopping)
    B --> C{Let me think}
    C -->|One| D[Laptop]
    C -->|Two| E[iPhone]
    C -->|Three| F[fa:fa-car Car]
`
mermaid`graph TD
list[List of Flights]
new[New Flight Form]
list-->|Create|new`
Select language

--- Step 2 ---

// Let's try and save some state to the window, to access in the next cell
window.someVariable = 10;

It appears that the JavaScript cell above is broken... How about an Observable JavaScript code block instead of a regular JavaScript block?

window.someVariable = 10

Success!

mermaid`graph TD
list[${window.someVariable}]
`

Success! This above is our first programmatic (though not yet dynamic, it's still "hard-coded") mermaid diagram 😊

Now, let's see if we can save to local storage:

;; source: https://gist.github.com/daveliepmann/cf923140702c8b1de301
(ns localstorage)
(defn set-item!
  "Set `key' in browser's localStorage to `val`."
  [key val]
  (.setItem (.-localStorage js/window) key val))
(defn get-item
  "Returns value of `key' from browser's localStorage."
  [key]
  (.getItem (.-localStorage js/window) key))
(defn remove-item!
  "Remove the browser's localStorage value for the given `key`"
  [key]
  (.removeItem (.-localStorage js/window) key))

Save to local storage w/ ClojureScript:

;; works as expected
(localstorage/set-item! "myAge" 5)

Read from local storage w/ ClojureScript:

;; works as expected
(localstorage/get-item "myAge")

--- Step 3 ---

Goal: Render a mermaid diagram dynamically

Let's say that we have a list of math topics and their learning dependencies.

;; first, let's try mapping topics as a pre-requisite topic to X other topics
#_(def math-deps
  [{"Addition 1" ["Addition 2" "Multiplication 0.5" "Subtraction 1"]}
   {"Addition 2" ["Addition 3" "Multiplication 1"]}
   {"Addition 3" ["Addition 4"]}
   {"Addition 4" ["Adding Decimals"]}
   {"Adding Decimals" ["Adding and subtracting negative numbers"]}
   {"Adding and subtracting negative numbers" []}
   {"Multiplication 0.5" ["Multiplication 1"]}
   {"Multiplication 1" ["Division 0.5" "Multiplication 1.5"]}
   {"Multiplication 1.5" ["Multiplication 2"]}
   {"Multiplication 2" ["Multiplication 3"]}
   {"Multiplication 3" []}
   {"Division 0.5" ["Division 1"]}
   {"Division 1" ["Division 1.5"]}
   {"Division 1.5" ["Division 2"]}
   {"Division 2" []}
   {"Subtraction 1" ["Subtraction 2"]}
   {"Subtraction 2" ["Subtraction 3"]}
   {"Subtraction 3" ["Subtraction 4"]}
   {"Subtraction 4" ["Subtracting decimals"]}
   {"Subtracting decimals" []}])

Let's render this vector of maps to mermaid syntax:

;; (map (fn make-mermaid [k v]
;;       (map #(str k "-->" %) v))
;;  math-deps)

I'm not getting the right syntax here. Let me break the problem down further:

Let's render one map into a list of mermaid strings:

;; investigate this code snippet
;; m = map, f = function, k = key
;; (defn update-map [m f]
;;   (reduce-kv (fn [m k v] 
;;     (assoc m k (f v))) {} m))
;; (def temp-map {"Addition 1" ["Addition 2" "Multiplication 0.5" "Subtraction 1"]})
(def temp-map-2 {"A[Addition 1]" ["B[Addition 2]" "C[Multiplication 0.5]" "D[Subtraction 1]"]})
;; (flatten (vec temp-map))
;; (flatten (vec temp-map-2))
(defn map-entry-to-mermaid [m]
  (filter
    (complement clojure.string/blank?)
    (clojure.string/split
      (reduce
        #(str %1 "\\n" (first (flatten (vec m))) " --> " %2 "\\n")
        ""
        (rest (flatten (vec m)))) #"\\n")))
(def temp-mer-2 (map-entry-to-mermaid temp-map-2))
;; looks like we need to convert the list to a newline delineated string
(def temp-mer-string-2
  (str (clojure.string/join \newline temp-mer-2)))
;; note: I had to use `\newline` rather than `\\n` to actually insert a valid newline into the string

I've remembered that Mermaid diagram nodes can must have a short text ID and (optionally) a longer text name (A: "Addition 1", B: "Addition 2", etc.)... Eh, this is fine for now. Let's try and render:

(localstorage/set-item! "_tempMerStr2" temp-mer-string-2)
window.localStorage
mermaid`graph TD
${window.localStorage._tempMerStr2}
`

Uh oh... It seems that the Mermaid wasn't well formed after all. It's back to the single letter approach. (The issue was that I wasn't giving the diagram nodes "short text IDs". Issue now resolved.)

Yay!

So, now that we've gotten a relatively simple mermaid diagram created programmatically and rendered, let's see if we can render a whole collection of items. Here is the collection from before, now correctly named and formatted:

;; temporarily paused
;; TODO: assess why this seems to glitch
(def math-deps-corrected
  [{"A[Addition 1]" ["B[Addition 2]" "G[Multiplication 0.5]" "P[Subtraction 1]"]}
   {"B[Addition 2]" ["C[Addition 3]" "H[Multiplication 1]"]}
   {"C[Addition 3]" ["D[Addition 4]"]}
   {"D[Addition 4]" ["E[Adding Decimals]"]}
   {"E[Adding Decimals]" ["F[Adding and subtracting negative numbers]"]}
   {"F[Adding and subtracting negative numbers]" []}
   {"G[Multiplication 0.5]" ["H[Multiplication 1]"]}
   {"H[Multiplication 1]" ["L[Division 0.5]" "I[Multiplication 1.5]"]}
   {"I[Multiplication 1.5]" ["J[Multiplication 2]"]}
   {"J[Multiplication 2]" ["K[Multiplication 3]"]}
   {"K[Multiplication 3]" []}
   {"L[Division 0.5]" ["M[Division 1]"]}
   {"M[Division 1]" ["N[Division 1.5]"]}
   {"N[Division 1.5]" ["O[Division 2]"]}
   {"O[Division 2]" []}
   {"P[Subtraction 1]" ["Q[Subtraction 2]"]}
   {"Q[Subtraction 2]" ["R[Subtraction 3]"]}
   {"R[Subtraction 3]" ["S[Subtraction 4]"]}
   {"S[Subtraction 4]" ["T[Subtracting decimals]"]}
   {"T[Subtracting decimals]" []}])

Now let's format this entire collection of entries (key-value pairs) into a single valid mermaid string.

;; converts one hashmap into one string with zero to many mermaid nodes
(defn hashmap-to-mermaid-string [hm]
  (str (clojure.string/join \newline (map-entry-to-mermaid hm))))
;; converts a vector of hashmaps into one string with zero to many mermaid nodes
(def mermaidString (clojure.string/join \newline (map hashmap-to-mermaid-string math-deps-corrected)))
(localstorage/set-item! "_merStr" mermaidString)
  • TODO: Make a function which takes graph type, graph orientation, and nodes text, and returns the entire mermaid input string

  • TODO: Render the graph so that it is rendered within the viewable space

  • TODO: Learn how to render in mermaid diagram nodes:

    • emoji

    • font-awesome

    • material-ui icons

// adjust mermaid rendering options to render within the available viewport
// note: This cell must be rerun if images are clipped
mermaidJs.initialize({
  flowchart:{
     useMaxWidth: true
  }
})
mermaid`graph TD
${window.localStorage._merStr}
`

Here's the reference image I am attempting to match 🙂

<image src="https://i.stack.imgur.com/I6O3U.png">

At last, I feel good to call it a day. So grateful I was able to get it to render 😂😴

Next time updates:

  • Style the graph to have a starry background, gray/blue/green/red node tiles, and thick colored lines in-between them

  • Clean up the logic to:

    • Firstly, create the nodes (without labels or styling)

    • Secondly, create the connections between the nodes

    • Thirdly, apply labels to the nodes

    • Fourthly, apply styling to the nodes

TBD: Render Test w/o set margins

// adjust mermaid rendering options to render within the available viewport
mermaidJs.initialize({
  flowchart:{
     useMaxWidth: false
  }
})
mermaid`graph TD
${window.localStorage._merStr}
`
// adjust mermaid rendering options to render within the available viewport
mermaidJs.initialize({
  flowchart:{
     useMaxWidth: true
  }
})
mermaid`graph TD
${window.localStorage._merStr}
`
Runtimes (4)