Mind Map Generator: From Text Parsing to SVG Visualization#

Published: May 8, 2026, 08:10

Mind maps are powerful tools for organizing thoughts and visualizing hierarchical relationships. Today, we’ll dive deep into implementing a text-based mind map generator, covering text parsing, tree layout algorithms, SVG rendering, and export optimization.

Core Technology Stack#

  • Text Parsing: Indentation-based hierarchy recognition
  • Layout Calculation: Recursive tree layout algorithm
  • Visualization: SVG Bezier curve connections
  • Export: Canvas API image generation

1. Text Parsing: From Indentation to Tree Structure#

The core challenge is converting indented text into a tree data structure. We use a stack-based parser:

interface Node {
  id: string;
  text: string;
  children: Node[];
  collapsed?: boolean;
  x?: number;  // Calculated layout coordinates
  y?: number;
}

function parseTree(text: string): Node {
  const lines = text.split("\n").filter(l => l.trim());
  const root: Node = { id: "root", text: "Mind Map", children: [] };
  const stack: { node: Node; level: number }[] = [{ node: root, level: -1 }];

  lines.forEach((line, idx) => {
    // Calculate indentation level (number of spaces)
    const level = line.search(/\S/);
    const text = line.trim();
    if (!text) return;

    const node: Node = { id: `node-${idx}`, text, children: [] };

    // Pop stack to correct parent level
    while (stack.length > 1 && stack[stack.length - 1].level >= level) {
      stack.pop();
    }

    // Add current node to parent's children
    stack[stack.length - 1].node.children.push(node);
    stack.push({ node, level });
  });

  return root;
}

Key insight: line.search(/\S/) gets the position of the first non-whitespace character, which equals the indentation level. The stack ensures correct parent-child relationships:

Project Planning
  Requirements
    User Research     # level = 4, parent is "Requirements"
    Competitive Analysis
  Design Phase        # level = 2, parent backtracks to "Project Planning"

2. Layout Algorithm: Recursive Position Calculation#

The main challenge in tree layout is avoiding node overlap while maintaining visual balance. We use a bottom-up recursive algorithm:

function calculateLayout(node: Node, x = 0, y = 0, levelHeight = 60) {
  const nodeWidth = Math.max(120, node.text.length * 14 + 32);

  if (node.children.length === 0 || node.collapsed) {
    return { node: { ...node, x, y }, width: nodeWidth };
  }

  // Recursively calculate all children layouts
  let totalChildrenWidth = 0;
  const childResults = [];

  for (const child of node.children) {
    const result = calculateLayout(child, 0, y + levelHeight, levelHeight);
    childResults.push(result);
    totalChildrenWidth += result.width;
  }

  // Add spacing between children
  totalChildrenWidth += (node.children.length - 1) * 40;

  // Horizontally center children
  const startX = x - totalChildrenWidth / 2;
  let currentX = startX;

  const updatedChildren = [];
  for (let i = 0; i < node.children.length; i++) {
    const childX = currentX + childResults[i].width / 2;
    const updatedChild = calculateLayout(
      node.children[i], childX, y + levelHeight, levelHeight
    );
    updatedChildren.push(updatedChild.node);
    currentX += childResults[i].width + 40;
  }

  return {
    node: { ...node, x, y, children: updatedChildren },
    width: Math.max(nodeWidth, totalChildrenWidth)
  };
}

Algorithm highlights:

  • Width pre-calculation: Recursively calculate subtree widths first, then determine positions
  • Center alignment: Children are centered around parent horizontally
  • Dynamic spacing: 40px gap between nodes prevents visual crowding

3. SVG Rendering: Bezier Curve Connections#

Connections use cubic Bezier curves for elegant visualization:

// Draw connections
{links.map((link, idx) => {
  const fromX = link.from.x || 0;
  const fromY = (link.from.y || 0) + 20;  // Node bottom
  const toX = link.to.x || 0;
  const toY = (link.to.y || 0) - 20;      // Child node top

  // Cubic Bezier curve control points
  const midY = (fromY + toY) / 2;
  const path = `M ${fromX} ${fromY} C ${fromX} ${midY}, ${toX} ${midY}, ${toX} ${toY}`;

  return (
    <path
      key={idx}
      d={path}
      fill="none"
      stroke="#4b5563"
      strokeWidth="2"
      opacity="0.6"
    />
  );
})}

Bezier curve principle: C x1 y1, x2 y2, x y defines three control points. Setting control points at the vertical midpoint creates elegant S-shaped curves.

Node rendering uses SVG <g> groups for scaling and translation:

{nodes.map((node, idx) => {
  const level = Math.floor((node.y || 0) / 60);
  const color = colors[level % colors.length];

  return (
    <g key={node.id} transform={`translate(${node.x}, ${node.y})`}>
      <rect
        x={-width / 2}
        y={-20}
        width={width}
        height={40}
        rx={8}
        fill={color}
        opacity="0.9"
      />
      <text
        x={0}
        y={5}
        textAnchor="middle"
        fill="white"
        fontSize="14"
      >
        {node.text}
      </text>
    </g>
  );
})}

4. Image Export: SVG to PNG#

Export functionality requires converting SVG to PNG, with proper Unicode handling:

function exportToPNG() {
  const svg = containerRef.current?.querySelector('svg');
  if (!svg) return;

  const svgData = new XMLSerializer().serializeToString(svg);
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image();

  img.onload = () => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx?.drawImage(img, 0, 0);

    const a = document.createElement('a');
    a.href = canvas.toDataURL('image/png');
    a.download = 'mindmap.png';
    a.click();
  };

  // Critical: Handle Unicode characters
  img.src = 'data:image/svg+xml;base64,' +
    btoa(unescape(encodeURIComponent(svgData)));
}

Encoding pitfall: btoa() only supports ASCII characters. Direct Chinese text input throws an error. The solution is encodeURIComponent for UTF-8 byte sequences, then unescape to restore ASCII string.

5. Performance Optimization & Edge Cases#

1. useMemo for Layout Caching#

const tree = useMemo(() => parseTree(input), [input]);
const layoutTree = useMemo(() => calculateLayout(tree).node, [tree]);
const { nodes, links } = useMemo(
  () => collectNodesAndLinks(layoutTree), [layoutTree]
);

2. Dynamic viewBox Calculation#

const bounds = useMemo(() => {
  const xs = nodes.map(n => n.x || 0);
  const ys = nodes.map(n => n.y || 0);
  return {
    minX: Math.min(...xs) - 100,
    minY: Math.min(...ys) - 50,
    maxX: Math.max(...xs) + 100,
    maxY: Math.max(...ys) + 100
  };
}, [nodes]);

3. Zoom Support#

const [scale, setScale] = useState(1);

<svg
  width={svgWidth * scale}
  height={svgHeight * scale}
  viewBox={`${bounds.minX} ${bounds.minY} ${svgWidth} ${svgHeight}`}
  style={{ transform: `scale(${scale})`, transformOrigin: 'top left' }}
>

Real-World Applications#

  1. Project Planning: Quickly outline requirements, design, development, and testing phases
  2. Knowledge Organization: Visualize tech stacks and framework features in tree structures
  3. Meeting Notes: Convert discussion points to structured mind maps in real-time
  4. Article Outlines: Plan chapter structures before writing

Implementing a mind map generator demonstrates the integration of text parsing, recursive algorithms, and SVG rendering. The core challenge lies in layout algorithm design—balancing node overlap prevention with visual harmony. Through stack-based parsing and bottom-up layout, we’ve built a clean, efficient mind map generator.