All posts
MermaidReal-TimeJavaScriptDashboards

Real-Time Mermaid Diagrams: Rendering Dynamic Data & Live Updates

7 min readThe MermaidCreator team

Static diagrams are snapshots; live diagrams are windows into your system right now. Whether you're visualizing server topology, rendering an active workflow, or showing real-time traffic flow, dynamic Mermaid rendering turns diagrams into living documentation that stays in sync with your data.

Why Real-Time Diagrams Matter

  • Operational visibility — engineers see system state without hunting through logs or dashboards
  • Live collaboration — teams see the same diagram as data changes, no refresh needed
  • Regulatory compliance — audit trails and flow diagrams stay current automatically
  • Teaching & demos — show a simulation running as a diagram that evolves frame-by-frame
  • Incident response — topology and state diagrams update during outages so responders see what's alive

Architecture: Polling vs. Streaming

Pattern 1: Polling (Simpler, Client-Driven)

Fetch new state at intervals; if it changed, re-render the diagram.

// Pseudo-code: React component with polling
import { useState, useEffect } from 'react';
import mermaid from 'mermaid';

export function LiveWorkflow({ workflowId }) {
  const [diagramCode, setDiagramCode] = useState('');

  useEffect(() => {
    const poll = setInterval(async () => {
      const res = await fetch(`/api/workflows/${workflowId}`);
      const { status, steps, activeStepId } = await res.json();
      
      // Generate Mermaid code from live data
      const code = generateFlowchart(steps, activeStepId);
      setDiagramCode(code);
    }, 2000); // Poll every 2 seconds

    return () => clearInterval(poll);
  }, [workflowId]);

  useEffect(() => {
    if (diagramCode) {
      mermaid.contentLoaded();
    }
  }, [diagramCode]);

  return <div className="mermaid">{diagramCode}</div>;
}

function generateFlowchart(steps, activeStepId) {
  let code = 'flowchart TD\n';
  steps.forEach((step) => {
    const style = step.id === activeStepId ? 'fill:#ffeb3b' : '';
    const status = step.status === 'done' ? '✓' : '';
    code += `  ${step.id}["${status} ${step.name}"]`;
    if (style) code += `\n  style ${step.id} ${style}`;
    code += '\n';
  });
  // Add edges...
  return code;
}

Pros: Simple, no server changes needed, stateless
Cons: Latency (up to 2s lag), wasted polls when data is static, scalability risk at high frequency

Use for: dashboards with 5–30 second tolerance, meeting demos, status boards

Pattern 2: WebSocket Streaming (Real-Time, Server-Driven)

Server pushes changes; client updates diagram immediately.

// React with WebSocket
export function LiveTopology({ clusterId }) {
  const [diagramCode, setDiagramCode] = useState('');

  useEffect(() => {
    const ws = new WebSocket(
      `wss://api.example.com/clusters/${clusterId}/stream`
    );

    ws.onmessage = (event) => {
      const delta = JSON.parse(event.data); // { type: 'node_up', node: {...} }
      // Merge delta into current state
      updateState(delta);
      // Re-render diagram
      const code = generateTopology(currentState);
      setDiagramCode(code);
    };

    return () => ws.close();
  }, [clusterId]);

  return <div className="mermaid">{diagramCode}</div>;
}

Pros: <100ms latency, server-driven (authoritative), scales to many clients
Cons: More server complexity, connection management, auth/TLS overhead

Use for: incident response, collaborative editing, production monitoring

Example: Live Workflow Execution

Here's a real workflow with dynamic state:

// Step data from your database
const workflowState = {
  steps: [
    { id: 'fetch', name: 'Fetch Data', status: 'done', duration: 245 },
    { id: 'validate', name: 'Validate', status: 'running', duration: 0 },
    { id: 'transform', name: 'Transform', status: 'pending', duration: 0 },
    { id: 'save', name: 'Save', status: 'pending', duration: 0 },
  ],
  activeStepId: 'validate',
  errors: [],
};

function generateWorkflowDiagram(state) {
  let code = 'flowchart TD\n';

  state.steps.forEach((step) => {
    const icon =
      step.status === 'done' ? '✓' :
      step.status === 'running' ? '⏱️' :
      '⏳';
    const label = `${icon} ${step.name} (${step.duration}ms)`;
    code += `  ${step.id}["${label}"]\n`;

    // Color by status
    const color =
      step.status === 'done' ? 'fill:#c8e6c9' :
      step.status === 'running' ? 'fill:#fff9c4' :
      'fill:#eceff1';
    code += `  style ${step.id} ${color}\n`;
  });

  // Connect steps
  state.steps.slice(0, -1).forEach((step, i) => {
    code += `  ${step.id} --> ${state.steps[i + 1].id}\n`;
  });

  return code;
}

Render output:

flowchart TD
    fetch["✓ Fetch Data (245ms)"]
    validate["⏱️ Validate (0ms)"]
    transform["⏳ Transform (0ms)"]
    save["⏳ Save (0ms)"]
    
    fetch --> validate --> transform --> save
    
    style fetch fill:#c8e6c9
    style validate fill:#fff9c4
    style transform fill:#eceff1
    style save fill:#eceff1

As the workflow progresses, activeStepId changes and the diagram updates in real-time.

Real-World Patterns

Pattern A: Infrastructure Topology (Monitoring)

// Live Kubernetes cluster state
async function fetchClusterTopology(clusterId) {
  const res = await fetch(`/api/clusters/${clusterId}/nodes`);
  const nodes = await res.json();

  let code = 'flowchart LR\n';
  code += `  Master["🎛️ Control Plane"]\n`;

  nodes.forEach((node) => {
    const status = node.ready ? '🟢' : '🔴';
    const label = `${status} ${node.name}<br/>(CPU: ${node.cpu}%, MEM: ${node.mem}%)`;
    code += `  ${node.id}["${label}"]\n`;
    
    if (node.ready) {
      code += `  style ${node.id} fill:#c8e6c9\n`;
    } else {
      code += `  style ${node.id} fill:#ffcdd2\n`;
    }

    code += `  Master --> ${node.id}\n`;
  });

  return code;
}

Use for: Kubernetes dashboards, server racks, mesh topology

Pattern B: Pipeline Progress (CI/CD)

// GitHub Actions / GitLab CI progress
function generatePipelineDiagram(pipeline) {
  let code = 'flowchart TD\n';

  const stages = ['build', 'test', 'deploy'];
  
  stages.forEach((stage, i) => {
    const jobs = pipeline.jobs.filter((j) => j.stage === stage);
    
    code += `  subgraph ${stage}["${stage.toUpperCase()}"]\n`;
    jobs.forEach((job) => {
      const status = job.status === 'success' ? '✓' : job.status === 'running' ? '⏳' : '✗';
      code += `    ${job.id}["${status} ${job.name}"]\n`;
    });
    code += `  end\n`;

    if (i < stages.length - 1) {
      code += `  ${stage} --> ${stages[i + 1]}\n`;
    }
  });

  return code;
}

Pattern C: Collaborative Diagram Editing (Live Sync)

// Shared editing session with WebSocket
export function CollaborativeDiagram({ diagramId }) {
  const [code, setCode] = useState('');
  const [collaborators, setCollaborators] = useState([]);

  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/diagrams/${diagramId}/collab`);

    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      
      if (msg.type === 'code_update') {
        setCode(msg.code); // Someone else edited; update locally
      } else if (msg.type === 'user_joined') {
        setCollaborators([...collaborators, msg.user]);
      }
    };

    return () => ws.close();
  }, [diagramId, collaborators]);

  const handleChange = (newCode) => {
    setCode(newCode);
    ws.send(JSON.stringify({ type: 'code_update', code: newCode }));
  };

  return (
    <>
      <div className="mermaid">{code}</div>
      <p>Editing: {collaborators.map((u) => u.name).join(', ')}</p>
    </>
  );
}

Performance Tips

1. Diff Before Re-Render

Don't regenerate and re-render on every poll. Only update if the Mermaid code actually changed:

const [prevCode, setPrevCode] = useState('');
const newCode = generateDiagram(state);

if (newCode !== prevCode) {
  setDiagramCode(newCode);
  setPrevCode(newCode);
  mermaid.contentLoaded();
}

2. Throttle Updates

Poll less frequently for non-critical dashboards:

const throttle = (fn, delay) => {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= delay) {
      fn(...args);
      last = now;
    }
  };
};

const handleUpdate = throttle(() => {
  // Re-render diagram
}, 1000); // Max once per second

3. Batch Updates

If multiple data sources change, collect them before re-rendering:

const updates = [];

ws.onmessage = (event) => {
  updates.push(JSON.parse(event.data));
};

setInterval(() => {
  if (updates.length > 0) {
    // Apply all updates at once
    const newState = applyDeltas(state, updates);
    setDiagramCode(generateDiagram(newState));
    updates.length = 0; // Clear batch
  }
}, 500); // Flush every 500ms

4. Limit Diagram Size

Keep live diagrams under 50 nodes; break complex topologies into views:

  • Cluster view → region-level nodes
  • Drill-down → click a region to see individual servers

Integration with MermaidCreator

Build and test live diagrams in MermaidCreator's code editor:

  1. Paste example state as JSON in comments
  2. Write the generation function
  3. Test it with a few state snapshots
  4. Export the code for integration

For collaborative editing on your team, upgrade to a workspace and use MermaidCreator's built-in Realtime presence + comments to see teammates editing live.

FAQ

How often should I poll? Start with 5 seconds; tune down to 2–3 seconds for dashboards, 500ms for incident response, 100ms only for real-time gaming/sim.

Will WebSocket updates work in my setup? Requires HTTPS + WSS (encrypted WebSocket). Most modern hosting (Vercel, Heroku, AWS) supports it. Polling works anywhere.

Can I animate transitions between states? Yes—use CSS transitions on the SVG elements Mermaid generates. Mermaid doesn't animate natively, but the SVG DOM does.

How do I handle errors or missed updates? For polling: on fetch error, show a stale-data banner and retry exponentially.
For WebSocket: on disconnect, fall back to polling until reconnected.

Real-time diagrams are the next frontier of ops tooling. Start with polling on a dashboard, then graduate to WebSocket when latency matters. Try a live workflow or cluster topology on MermaidCreator today.

Related posts