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`
--- 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" []}])
;; second, let's try mapping topics in reverse to above, as, "what is this node a child of?" (ie. the value is a list of parents)
(def math-deps-2
{{"Addition 1" []} ;; this is the root node, so we start here
{"Addition 2" ["Addition 1"]}
{"Addition 3" []}
{"Addition 4" []}
{"Adding Decimals" []}
{"Adding and subtracting negative numbers" []}
{"Multiplication 0.5" ["Addition 1"]}
{"Multiplication 1" []}
{"Multiplication 1.5" []}
{"Multiplication 2" []}
{"Multiplication 3" []}
{"Division 0.5" []}
{"Division 1" []}
{"Division 1.5" []}
{"Division 2" []}
{"Subtraction 1" ["Addition 1"]}
{"Subtraction 2" []}
{"Subtraction 3" []}
{"Subtraction 4" []}
{"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}
`