UIBUILDER: Using MermaidJS

Hi all, little mini FAQ about using MermaidJS to generate browser-based charts with data from Node-RED and with the support of UIBUILDER.

You will see some chat here: StateTrail-Node for Dashboard 2.0 - about using Mermaid for a timeline chart. It supports many other chart types too.

Mermaid takes TEXT chart descriptions and turns them into graphics. Usually it is used as an extension along side some tool to convert Markdown to HTML. If I get time, I'll add more info to this FAQ to show how to do that as well since UIBUILDER supports Markdown quite readily. But initially, I'll just show you 1 way to use the Mermaid library directly.

UIBUILDER progressively enhances native web tooling and so supports any web library. :wink:

[{"id":"65bbdbc1a8301e7b","type":"uibuilder","z":"1b2ed12a321034dc","name":"","topic":"","url":"mermaid-timeline","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"esm-blank-client","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"7.1.0","showMsgUib":true,"title":"","descr":"","editurl":"vscode://file/src/uibRoot/mermaid-timeline/?windowId=_blank","x":400,"y":340,"wires":[["a4004d1d51fe87d4"],[]]},{"id":"0980fae560b35fd6","type":"debug","z":"1b2ed12a321034dc","name":"uibuilder standard output","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":703.0000305175781,"y":386.9999895095825,"wires":[],"l":false},{"id":"f4a8f0029f21b813","type":"link in","z":"1b2ed12a321034dc","name":"in to uib","links":["1e40e8ac1c2ee858"],"x":245,"y":340,"wires":[["65bbdbc1a8301e7b"]]},{"id":"a4004d1d51fe87d4","type":"switch","z":"1b2ed12a321034dc","name":"","property":"_ui.id","propertyType":"msg","rules":[{"t":"eq","v":"btn1","vt":"str"},{"t":"eq","v":"btn2","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":3,"x":590,"y":340,"wires":[["562c9d4ecec8efde"],["53ea9de0b8b3528a"],["0980fae560b35fd6"]]},{"id":"1e40e8ac1c2ee858","type":"link out","z":"1b2ed12a321034dc","name":"out to uib","mode":"link","links":["f4a8f0029f21b813"],"x":1045,"y":320,"wires":[]},{"id":"4571a4fe83cfd5b5","type":"template","z":"1b2ed12a321034dc","name":"index.html","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!doctype html>\n<html lang=\"en\"><head>\n\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"icon\" href=\"../uibuilder/images/node-blue.ico\">\n\n    <title>Markdown-Mermaid Example - Node-RED uibuilder</title>\n    <meta name=\"description\" content=\"Node-RED uibuilder - Markdown-Mermaid Example\">\n\n    <!-- Your own CSS -->\n    <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n    <script defer src=\"https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js\"></script>\n    <script defer src=\"../uibuilder/uibuilder.iife.min.js\"></script>\n    <script defer src=\"./index.js\"></script>\n\n</head><body class=\"uib\">\n    \n    <h1 class=\"with-subtitle\">Markdown-Mermaid Example</h1>\n    <div role=\"doc-subtitle\">Using the uibuilder IIFE library.</div>\n\n    <div>\n        <button id=\"btn1\" onclick=\"uibuilder.eventSend(event)\">Machine-1</button>\n        <button id=\"btn2\" onclick=\"uibuilder.eventSend(event)\">Machine-2</button>\n    </div>\n\n    <!-- <pre id=\"chart\"></pre> -->\n    <pre id=\"chart\" class=\"mermaid\"></pre>\n\n    <div id=\"more\"></div>\n\n</body></html>\n","output":"str","x":910,"y":460,"wires":[["57d61672e9bf3aaf"]]},{"id":"d35ddf5578ec138c","type":"inject","z":"1b2ed12a321034dc","name":"Upd FE Code (Run this once to get the web page)","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":590,"y":460,"wires":[["4571a4fe83cfd5b5","dfb618a2f72fbebd"]]},{"id":"57d61672e9bf3aaf","type":"uib-save","z":"1b2ed12a321034dc","url":"mermaid-timeline","uibId":"65bbdbc1a8301e7b","folder":"src","fname":"index.html","createFolder":false,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"index.html","topic":"","x":1070,"y":460,"wires":[]},{"id":"dfb618a2f72fbebd","type":"template","z":"1b2ed12a321034dc","name":"index.js","field":"payload","fieldType":"msg","format":"javascript","syntax":"mustache","template":"// @ts-nocheck\n\n// console.log(mermaid) // This is the mermaid library object\n\n// Don't let the library start automatically - we need control\nmermaid.initialize({ startOnLoad: false });\n\nconst printArguments = function (arg1, arg2, arg3) {\n    alert('printArguments called with arguments: ' + arg1 + ', ' + arg2 + ', ' + arg3);\n};\n\n// Listen for incoming messages from Node-RED and action\nuibuilder.onChange('msg', (msg) => {\n    // console.log(msg.payload)\n\n    // Get a reference to the chart element\n    const chart = document.getElementById('chart')\n\n    // If the chart has already been processed, remove it\n    delete chart.dataset.processed\n    chart.innerHTML = ''\n\n    // Set the innerHTML of the chart element to the payload\n    $('#chart').textContent = msg.payload\n\n    // (Re-)run the mermaid code\n    mermaid.run({ querySelector: '#chart' });\n})\n","output":"str","x":900,"y":520,"wires":[["c6f4ee3f3127719b"]]},{"id":"c6f4ee3f3127719b","type":"uib-save","z":"1b2ed12a321034dc","url":"mermaid-timeline","uibId":"65bbdbc1a8301e7b","folder":"src","fname":"index.js","createFolder":false,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"index.js","topic":"","x":1060,"y":520,"wires":[]},{"id":"e03baf0d7e66ba6a","type":"template","z":"1b2ed12a321034dc","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"  \n---\ndisplayMode: compact\n---\ngantt\n  dateFormat YYYY-MM-DD HH:mm\n  axisFormat %H:%M\n  tickInterval 1hour\n  title Machine Operation Status (HTML) \n  \n  section {{machine}}\n    {{data}}\n  \n  click des1 call printArguments(\"test1\", \"test2\", test3)\n","output":"str","x":940,"y":320,"wires":[["1e40e8ac1c2ee858"]]},{"id":"562c9d4ecec8efde","type":"function","z":"1b2ed12a321034dc","name":"Machine 1 data","func":"msg.data = `\n\nOFF :crit, des1, 2014-12-15 00:00, 2014-12-15 01:10\nON :active, des2, 2014-12-15 01:10, 2014-12-15 02:20\nOFF :crit, des1, 2014-12-15 02:20, 2014-12-15 09:30\nON :active, des2, 2014-12-15 09:30, 2014-12-15 14:00\n\n`\n\nmsg.machine = 'machine-1'\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":300,"wires":[["e03baf0d7e66ba6a"]]},{"id":"53ea9de0b8b3528a","type":"function","z":"1b2ed12a321034dc","name":"Machine 2 data","func":"msg.data = `\n    OFF :active, des1, 2014-12-15 00:00, 2014-12-15 01:10\n    ON :crit, des2, 2014-12-15 01:10, 2014-12-15 02:20\n    OFF :active, des1, 2014-12-15 02:20, 2014-12-15 09:30\n    ON :crit, des2, 2014-12-15 09:30, 2014-12-15 14:00\n`\n\nmsg.machine = 'machine-2'\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":340,"wires":[["e03baf0d7e66ba6a"]]}]

After setting up the uibuilder and uib-save nodes and deploying. Trigger the inject to get the desired web page and script. The page looks like this:

Press one of the buttons to get the chart:

Then press the other button to get the other chart. The data comes from link to the other thread.

The 2 function nodes set up the text data for each machine output and the template merges the data into the Mermaid chart description text. The full text looks like this:

  
---
displayMode: compact
---
gantt
  dateFormat YYYY-MM-DD HH:mm
  axisFormat %H:%M
  tickInterval 1hour
  title Machine Operation Status (HTML) 
  
  section machine-1
    

OFF :crit, des1, 2014-12-15 00:00, 2014-12-15 01:10
ON :active, des2, 2014-12-15 01:10, 2014-12-15 02:20
OFF :crit, des1, 2014-12-15 02:20, 2014-12-15 09:30
ON :active, des2, 2014-12-15 09:30, 2014-12-15 14:00

Note the initial blank line - you need that otherwise Mermaid objects to the YAML displayMode instruction.

Also note that Mermaid is essentially a STATIC chart. You cannot dynamically extend the chart data, you can only completely rebuild the chart. But with the magic of Node-RED and UIBUILDER, rebuilding the chart is simple.

This is a very simplistic example but it certainly works. Not sure whether I'd ever actually use this outside of a Markdown page but the approach could occasionally be helpful.

2 Likes

Works as expected indeed.
How do we get the on click event ?

I am able to get it to open a website (as in example), but unable to get the data within the chart (start time/end time) etc..

uib-st

A good question. The Mermaid documentation looks good but in fact has some significant issues. I've not managed to get the events to work but I've not had time to dig into the code to find out what is happening. I've tried some of their code examples and some of them don't work at all - I think because they come from an older version of the library. There are also oddities when using the displayMode option because if you aren't really careful, the library rejects it as being invalid.

Also, the Mermaid stuff is very static - you cannot extend the diagram, only fully rebuild.


One other thing this has done is to highlight an issue with the Markdown-IT library that is directly supported by uibuilder. It seems that all of the Mermaid extensions for Markdown-IT have issues.


Really, I still think that a web component is the right answer, I just need to find some time to get it written. Christmas time is not necessarily the best time to focus on this stuff! But I will try to find some more time. :slight_smile:

you need to add the javascript part:

 <script>
    const printArguments = function (arg1, arg2, arg3) {
      alert('printArguments called with arguments: ' + arg1 + ', ' + arg2 + ', ' + arg3);
    };
    const printTask = function (taskId) {
      alert('taskId: ' + taskId);
    };
    const config = {
      startOnLoad: true,
      securityLevel: 'loose',
    };
    mermaid.initialize(config);
  </script>

the functions can be called as defined.

OK, I actually worked it out just after I wrote my last message. You need to add a security level setting:

// Don't let the library start automatically - we need control. Also enable events
mermaid.initialize({
    startOnLoad: false,
    securityLevel: 'loose',
})

Also, you need to assign the callback function to the global object. So instead of const printArguments = function ...., you need:

window.printArguments = function (arg1, arg2, arg3) {
    console.warn('printArguments called with arguments: ' + arg1 + ', ' + arg2 + ', ' + arg3)
}

Also note:

  • If you call the function with no arguments, it gets the id of the clicked area. If you pass arguments, you don't get that, just the things you passed.
  • The example data repeats an ID - if you do that, you only get an event on the first block with that id. So really, you need to give each entry a different, unique id.

The good news about doing this with uibuilder is that you can use uibuilder front-end functions:

click des2 call uibuilder.send({topic: "des2 clicked", payload: 42})

The bad news is that it really is not a scalable option.