Grouping Mermaid nodes in Subgraphs ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Mermaid provides some really great ways to group or fence in parts of your graphs through the use of subgraphs. Date: March 5, 2022 Mermaid provides some really great ways to group or fence in parts of your graphs through the use of subgraphs. Here we can model some sort of data ingest with some raw iot device and our warehouse in different groups. ``` graph TD; subgraph raw_iot a end subgraph warehouse A --> B B --> C end ``` graph TD; ``` subgraph raw_iot a end subgraph warehouse A --> B B --> C end ``` connecting subgroups ──────────────────── If we want to connect them, we can make a connection between a and A outside of the subgraphs. ``` graph TD; subgraph raw_iot a end a --> A subgraph warehouse A --> B B --> C end ``` graph TD; ``` subgraph raw_iot a end a --> A subgraph warehouse A --> B B --> C end ``` separation of concerns ────────────────────── It’s also possible to specify subgraphs separate from where you define your nodes. which allows for some different levels of grouping that would not be possible if you were to define all your nodes inside of a subgraph. ``` graph TD; a --> A A --> B B --> C subgraph one A C end ``` graph TD; a --> A A --> B B --> C ``` subgraph warehouse A C end ``` import mermaid from '/assets/vendor/mermaid/mermaid.esm.min.mjs'; const rootStyle = getComputedStyle(document.documentElement); const css = (name, fallback) => (rootStyle.getPropertyValue(name) || fallback).trim(); const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches || document.documentElement.dataset.theme === 'dark'; const accent = css('--color-primary', '#ffcd11'); const flowchart = { nodeSpacing: 60, rankSpacing: 90, padding: 12, }; const themeCSS = ` .label foreignObject > div { padding: 14px 14px 10px; line-height: 1.2; } .nodeLabel { padding: 14px 14px 10px; line-height: 1.2; } * { cursor: pointer; } `; const themeVariables = { background: css('--color-background', '#ffffff'), primaryColor: css('--color-code-bg', '#0a0a0a'), primaryTextColor: css('--color-text', '#1f2937'), primaryBorderColor: accent, lineColor: accent, textColor: css('--color-text', '#1f2937'), nodeBkg: css('--color-code-bg', '#0a0a0a'), nodeBorder: accent, nodeTextColor: css('--color-text', '#1f2937'), fontSize: '16px', nodePadding: 20, nodeTextMargin: 14, clusterBkg: isDark ? css('--color-background', '#0f0f0f') : css('--color-surface', '#f9fafb'), clusterBorder: accent, clusterTextColor: css('--color-text', '#1f2937'), titleColor: css('--color-text', '#1f2937'), edgeLabelBackground: css('--color-code-bg', '#0a0a0a'), }; const SVG_PAN_ZOOM_CDN = 'https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/dist/svg-pan-zoom.min.js'; let mermaidLightbox = null; let activePanZoom = null; // Inject lightbox styles once const injectLightboxStyles = () => { if (document.getElementById('mermaid-lightbox-css')) return; const style = document.createElement('style'); style.id = 'mermaid-lightbox-css'; style.textContent = ` /* Container fills the GLightbox slide */ .mermaid-lightbox-wrap { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: transparent; position: relative; } .mermaid-lightbox-wrap svg { width: 100% !important; height: 100% !important; max-width: 100%; max-height: 100%; } /* Hide GLightbox prev/next arrows (single-slide lightbox) */ .glightbox-container .gprev, .glightbox-container .gnext { display: none !important; } /* Hide description area that renders as a white box */ .glightbox-container .gslide-description, .glightbox-container .gslide-title, .glightbox-container .gdesc-inner, .glightbox-container .gslide-desc { display: none !important; } /* Remove white background from inline slide content */ .glightbox-container .gslide-inline { background: transparent !important; } /* Make the inline content area fill the slide */ .glightbox-container .ginlined-content { max-width: none !important; max-height: none !important; width: 100%; height: 100%; padding: 0 !important; } /* Remove box-shadow from the media container */ .glightbox-container .gslide-media { box-shadow: none !important; } /* Toolbar styling */ .mermaid-lightbox-toolbar { position: absolute; top: 8px; right: 8px; z-index: 10; display: flex; gap: 4px; } .mermaid-pz-btn { background: rgba(0,0,0,0.6); color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 14px; line-height: 1; } .mermaid-pz-btn:hover { background: rgba(0,0,0,0.8); border-color: rgba(255,255,255,0.6); } `; document.head.appendChild(style); }; // Lazy-load svg-pan-zoom from CDN, returns a promise const loadSvgPanZoom = () => { if (typeof svgPanZoom !== 'undefined') return Promise.resolve(); return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = SVG_PAN_ZOOM_CDN; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); }; // Initialize svg-pan-zoom on the SVG inside the lightbox. // Retries until the lightbox container has settled dimensions. let _pzRetries = 0; const initPanZoom = () => { if (activePanZoom) return; const container = document.querySelector('.glightbox-container .gslide.current .mermaid-lightbox-wrap'); if (!container) return; const svgEl = container.querySelector('svg'); if (!svgEl) return; // Ensure the container has layout dimensions before initializing. const cRect = container.getBoundingClientRect(); if (cRect.width < 10 || cRect.height < 10) { if (_pzRetries < 20) { _pzRetries++; setTimeout(initPanZoom, 50); } return; } // svg-pan-zoom needs a viewBox. Pre-rendered SVGs from mermaid // usually have one; browser-rendered ones may not. if (!svgEl.getAttribute('viewBox')) { let w = parseFloat(svgEl.getAttribute('width')); let h = parseFloat(svgEl.getAttribute('height')); if (!w && svgEl.style.maxWidth) w = parseFloat(svgEl.style.maxWidth); if (!w || !h) { const r = svgEl.getBoundingClientRect(); if (!w) w = r.width; if (!h) h = r.height; } if (w > 0 && h > 0) { svgEl.setAttribute('viewBox', '0 0 ' + w + ' ' + h); } else if (_pzRetries < 20) { _pzRetries++; setTimeout(initPanZoom, 50); return; } } _pzRetries = 0; // Clear inline dimensions so SVG can be sized by the container // and svg-pan-zoom can manage transforms. svgEl.removeAttribute('width'); svgEl.removeAttribute('height'); svgEl.style.cssText = 'width:100%;height:100%;'; try { activePanZoom = svgPanZoom(svgEl, { zoomEnabled: true, panEnabled: true, controlIconsEnabled: false, fit: true, center: true, contain: false, minZoom: 0.3, maxZoom: 10, zoomScaleSensitivity: 0.3, mouseWheelZoomEnabled: true, preventMouseEventsDefault: true, }); // Double-check fit after a frame in case dimensions shifted requestAnimationFrame(() => { if (!activePanZoom) return; activePanZoom.resize(); activePanZoom.fit(); activePanZoom.center(); }); } catch (_) { activePanZoom = null; } // Add reset/fit buttons let toolbar = container.querySelector('.mermaid-lightbox-toolbar'); if (!toolbar) { toolbar = document.createElement('div'); toolbar.className = 'mermaid-lightbox-toolbar'; toolbar.innerHTML = '' + '' + ''; toolbar.addEventListener('click', (ev) => { const btn = ev.target.closest('[data-action]'); if (!btn || !activePanZoom) return; ev.preventDefault(); ev.stopPropagation(); const action = btn.dataset.action; if (action === 'fit') { activePanZoom.resize(); activePanZoom.fit(); activePanZoom.center(); } else if (action === 'zoomin') { activePanZoom.zoomIn(); } else if (action === 'zoomout') { activePanZoom.zoomOut(); } }); container.prepend(toolbar); } }; // Destroy pan-zoom on lightbox close const destroyPanZoom = () => { if (activePanZoom) { try { activePanZoom.destroy(); } catch (_) { /* no-op */ } activePanZoom = null; } }; let _lbRetries = 0; const ensureMermaidLightbox = () => { const diagrams = document.querySelectorAll('.mermaid svg'); if (!diagrams.length) { // Mermaid ESM may still be rendering -- retry up to 2s if (_lbRetries < 20) { _lbRetries++; setTimeout(ensureMermaidLightbox, 100); } return; } _lbRetries = 0; injectLightboxStyles(); diagrams.forEach((svg) => { if (svg.dataset.lightboxBound) return; svg.dataset.lightboxBound = 'true'; svg.style.cursor = 'pointer'; svg.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const svgHtml = svg.outerHTML; const openLightbox = () => { if (!mermaidLightbox) { mermaidLightbox = GLightbox({ selector: false, openEffect: 'fade', closeEffect: 'fade', zoomable: false, draggable: false, skin: 'clean', }); mermaidLightbox.on('slide_after_load', () => { destroyPanZoom(); _pzRetries = 0; loadSvgPanZoom().then(() => initPanZoom()); }); mermaidLightbox.on('close', destroyPanZoom); } mermaidLightbox.setElements([{ content: '
' + svgHtml + '
', width: '90vw', height: '90vh' }]); mermaidLightbox.open(); loadSvgPanZoom(); }; if (typeof GLightbox !== 'undefined') { openLightbox(); } else if (window.initGLightbox) { window.initGLightbox(); openLightbox(); } else { window.addEventListener('glightbox-ready', () => { openLightbox(); }, { once: true }); } }); }); }; mermaid.initialize({ startOnLoad: false, theme: 'base', themeVariables, flowchart, themeCSS }); window.initMermaid = async () => { try { await mermaid.run(); } catch (e) { console.error('mermaid.run failed:', e); } ensureMermaidLightbox(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => window.initMermaid()); } else { window.initMermaid(); }