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 renderedmermaid` 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 TDlist[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 cellwindow.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 = 10Success!
mermaid`graph TDlist[${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 stringI'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.localStoragemermaid`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 clippedmermaidJs.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 viewportmermaidJs.initialize({ flowchart:{ useMaxWidth: false }})mermaid`graph TD${window.localStorage._merStr}`// adjust mermaid rendering options to render within the available viewportmermaidJs.initialize({ flowchart:{ useMaxWidth: true }})mermaid`graph TD${window.localStorage._merStr}`