Teil 6 · Der Mindmap-Renderer: Was mich das händische Bauen von SVG gelehrt hat (und warum v2 React Flow nutzt)

Mai 15, 2026·
Ndimofor Aretas
Ndimofor Aretas
· 8 Min Lesezeit
blog Frontend

Teil einer Serie über die Entwicklung von Gemma CogniVault. Zuvor: Getting reliable JSON out of a local LLM.

Alle Abkürzungen werden im Anhang unten auf der Seite vollständig erklärt.

Der Study Hub von CogniVault hat vier Modi. Drei davon – Quiz, Workshop, Flashcards – haben eine Listenform. Der vierte, Mindmap, nicht. Es ist ein Baum von Konzepten, die von einem zentralen Thema ausgehen, und ich wollte, dass er:

  • Optisch so sauber ist, dass der Nutzer ihn sich tatsächlich gerne ansieht.
  • Interaktiv ist: Verschieben, Zoomen, Erkunden.
  • Hochauflösend als PNG und PDF exportiert werden kann.

Dieser Beitrag ist die ehrliche Version davon, wie dieser Renderer gebaut wurde – und zwar zweimal. Version eins war reines, händisch gebautes SVG ohne Graphen-Bibliothek, und sie wurde veröffentlicht. Version zwei, die heute in der Codebasis steht, baut auf @xyflow/react (React Flow) und einem dagre Auto-Layout auf. Ich glaube, beide Entscheidungen waren zu dem Zeitpunkt, als sie getroffen wurden, richtig. Und der Weg dazwischen hat mir mehr über “Build vs. Buy” (Selber bauen oder einkaufen) beigebracht als jede der beiden Versionen für sich genommen.

Runde eins: Händisch bauen

Mein erster Instinkt war, wie bei jedem anderen auch, sofort an Tag eins zu einer Bibliothek zu greifen. Ich habe widerstanden, und zwar aus guten Gründen: Das Standard-Styling hätte sowieso komplett angepasst werden müssen, das gewünschte Layout war simpel, für den Export hätte man ohnehin externe Abhängigkeiten gebraucht, und die Bundle-Größe ist auch nicht zu verachten. Für die kleinen Bäume, die Gemma generiert, schien SVG völlig ausreichend – es lässt sich mit dem viewBox-Attribut verschieben und zoomen, zeichnet beliebige Formen, lässt sich als String serialisieren und sauber rastern.

Also war v1 reines SVG. Und der Kern davon war wirklich klein.

Radial-Layout in 40 Zeilen

Ein radiales Layout platziert die Wurzel in der Mitte und ordnet die Kinder in konzentrischen Ringen an:

type Node = { id: string; label: string; children: Node[] };
type Placed = Node & { x: number; y: number; angle: number };

function layout(root: Node, radiusStep = 180): Placed[] {
  const placed: Placed[] = [];

  function place(
    node: Node,
    depth: number,
    fromAngle: number,
    toAngle: number,
  ) {
    if (depth === 0) {
      placed.push({ ...node, x: 0, y: 0, angle: 0 });
    } else {
      const angle = (fromAngle + toAngle) / 2;
      placed.push({
        ...node,
        x: depth * radiusStep * Math.cos(angle),
        y: depth * radiusStep * Math.sin(angle),
        angle,
      });
    }

    const slice = (toAngle - fromAngle) / Math.max(node.children.length, 1);
    node.children.forEach((child, i) =>
      place(
        child,
        depth + 1,
        fromAngle + i * slice,
        fromAngle + (i + 1) * slice,
      ),
    );
  }

  place(root, 0, 0, 2 * Math.PI);
  return placed;
}

Jede Ebene erbt ein Winkelsegment von ihrem Elternteil und unterteilt es unter ihren Kindern. Verschieben (Pan) und Zoomen waren reine viewBox-Arithmetik – keine Transformationsmatrizen, keine Event-Bibliothek, nur Zahlen. Kanten waren quadratische Bézier-Kurven, die in Richtung Zentrum gezogen wurden. Es sah gut aus, war schnell, und der gesamte Renderer passte bequem in eine einzige Komponente.

Der Export-Trick, den man immer noch kennen sollte

Um ein SVG ohne Abhängigkeiten als PNG zu exportieren, lässt man den Browser die ganze Arbeit machen:

SVG DOM  ─►  XMLSerializer ─►  string  ─►  <img>  ─►  <canvas>  ─►  PNG blob

Serialisiere das SVG zu einem String, lade es in ein Image, zeichne dieses Bild auf ein skaliertes <canvas> und frag das Canvas nach einem PNG. Schriftarten, Anti-Aliasing, currentColor – der Browser löst das alles nativ auf. Wenn deine Grafik ein SVG-Element ist, ist das immer noch die sauberste Export-Pipeline, die es gibt, und ich würde sie ohne Zögern wieder verwenden.

Ein Detail von v1 hat bis heute komplett unverändert überlebt: der Speichern-Flow. Anstatt des klassischen “Direkt in den Downloads-Ordner”-Erlebnisses laufen Exporte über die File System Access API (showSaveFilePicker), wo der Browser das unterstützt, mit einem Anker-Tag-Download-Fallback für Firefox und Safari. Ein echter “Speichern unter…"-Dialog, ganz ohne Electron. Dieser Helfer (lib/saveBlob.ts) bedient jetzt auch die Quiz- und Workshop-Exporte.

Die Anforderungen, die v1 gebrochen haben

Dann traf das Feature auf seine Nutzer (naja – auf mich, als ich es beim Lernen ernsthaft benutzte), und es tauchten drei Anforderungen auf, mit denen die elegante, handgemachte Version nicht gut zurechtkam:

  1. “Lass mich diesen Knoten bewegen.” Ein generiertes Layout ist ein Startpunkt; eine nützliche Mindmap ist eine, die du so anordnest, dass sie deiner Denkweise entspricht. Die Knoten von v1 waren an ihren berechneten Positionen fixiert. Drag-and-Drop hinzuzufügen hätte bedeutet, Hit-Testing, Drag-State und Positions-Persistenz von Grund auf neu zu bauen – genau die unglamouröse Interaktions-Maschinerie, für die Graphen-Bibliotheken überhaupt existieren.

  2. Text wollte HTML sein. SVG <text> bricht nicht um. Lange Konzept-Labels erforderten manuelles Umbrechen, Ausmessen und Abschneiden von Zeilen – ein ständiger Kampf. HTML-Knoten (echte <div>s mit CSS) übernehmen Zeilenumbrüche, Ellipsen und Theming umsonst.

  3. Radial war doch nicht das beste Lese-Layout. Für die breiten und flachen Bäume, die Gemma tatsächlich generiert, liest sich ein Links-nach-Rechts- oder Von-Oben-nach-Unten-Baum (die Art, die eine Layout-Engine wie dagre berechnet) besser als Ringe. Und als Layouts umschaltbar wurden, wurde “Auto-Layout plus gemerkte manuelle Anpassungen” das natürliche Modell.

Ich hätte all das auf der SVG-Basis bauen können. Aber schau dir die Liste an: Viewport-Management, Drag-and-Drop für Knoten, HTML-Knoten in einem Graphen-Canvas, austauschbare Layouts. Das ist exakt das Feature-Set von React Flow. In v1 wäre die Bibliothek nur ein Wrapper um Dinge gewesen, die ich nicht brauchte. Für v2 waren meine Anforderungen genau zu den Dingen herangewachsen, die sie gut kann.

Also habe ich meine Meinung geändert.

Runde zwei: React Flow + dagre

Der heutige Renderer (frontend/src/components/study/mindmaps/):

  • @xyflow/react (React Flow) liefert das Canvas: natives Panning/Zoomen, ziehbare Knoten, einen Steuerungsblock im Minimap-Stil und Dark-Mode-Unterstützung via colorMode.
  • dagre berechnet das automatische Layout, mit einem sichtbaren Schalter zwischen Links-nach-Rechts und Von-Oben-nach-Unten. Die Baum-zu-Graph-Konvertierung ist eine winzige Pure Function.
  • Benutzerdefinierte HTML-Knoten tragen das Design-System: Die Wurzel bekommt einen Verlauf, Themen eine Tönung, Blätter bleiben dezent – und Text bricht um, wie Text umbrechen sollte.
  • Gezogene Positionen bleiben erhalten. Das Bewegen eines Knotens löst einen Speicher-Vorgang aus; öffnest du die Map neu, wird deine Anordnung wiederhergestellt. Ein “Reset layout”-Button löscht die gespeicherten Positionen und kehrt zum dagre-Auto-Layout zurück. Die Layout-Wahl und die Positionen leben zusammen mit der Mindmap in SQLite.
  • Der Export wurde an die neue Realität angepasst. Die Knoten sind jetzt HTML, also funktioniert der SVG-Serialisierungs-Trick aus v1 nicht mehr. Der PNG-Export nutzt html-to-image über den React-Flow-Viewport, passgenau auf die Knotengrenzen zugeschnitten, unabhängig vom aktuellen Zoom. Das PDF bettet dieses PNG über ein lazy-geladenes jsPDF ein. Der Markdown-Export ist ein rekursiver Durchlauf des Baums ohne externe Abhängigkeiten. Ja – v2 nutzt genau die Bibliothek (html-to-image), auf deren Vermeidung ich in v1 noch stolz war. Die Anforderungen haben sich geändert; und damit auch der Trade-off.

Was mich die Reise wirklich gelehrt hat

Ich habe hin- und herüberlegt, wie ich diesen Beitrag schreiben soll, denn die Geschichte von v1 (“Schau mal, wie wenig Code man braucht!”) ist schmeichelhafter. Aber die Wahrheit aus beiden Versionen ist die viel nützlichere Lektion:

  1. Zuerst selber bauen war trotzdem richtig. v1 war an einem Wochenende fertig, hat mir die wahre Form des Problems gezeigt (Layout, Viewport und Export sind getrennte Baustellen) und kostete mich nichts, es wegzuwerfen, weil es so klein war. Hätte ich mit React Flow angefangen, hätte ich eine Bibliothek konfiguriert, bevor ich das Problem überhaupt verstanden hatte.

  2. Bibliotheken verdienen sich ihren Platz erst, wenn deine Anforderungen auf ihr Feature-Set zulaufen – nicht vorher. In dem Moment, als “Zieh Knoten und merk dir, wo ich sie hingelegt habe” zur Anforderung wurde, drehte sich die Build-vs-Buy-Rechnung komplett um.

  3. Einige Teile überleben das Rewrite. Der Speicher-Dialog-Helfer, der Markdown-Durchlauf, der Instinkt, Exporte auf die Inhaltsgrenzen zuzuschneiden – all das wurde übernommen. Rewrites sind selten total.

  4. Die nativen Pipelines des Browsers sollte man kennen, auch wenn man sie am Ende nicht nutzt. SVG → Canvas → PNG ist immer noch der beste Export-Trick ohne externe Abhängigkeiten in der Frontend-Entwicklung. Er funktioniert nur nicht mehr an dem Tag, an dem deine Knoten HTML werden.

Fazit

“Build or buy” (Selber bauen oder kaufen) ist eine Funktion von Anforderungen – und Anforderungen bewegen sich. Bau selbst, solange das Problem klein ist und du noch lernst, wie es aussieht. Kauf ein, wenn deine Feature-Liste anfängt, sich wie die README der Bibliothek zu lesen. Und wenn du wechselst, schreib auf, warum, damit die nächste Person (oder dein zukünftiges Ich) weiß, dass es keine Unentschlossenheit war. Es war einfach der Plan, der erwachsen geworden ist.


Anhang: Abkürzungen in diesem Beitrag

AbkürzungVollformBedeutung
SVGScalable Vector GraphicsDas eingebaute Vektor-Zeichenformat des Browsers – das komplette Fundament von v1
PNGPortable Network GraphicsDas Rasterbildformat, das bei Exporten herauskommt
PDFPortable Document FormatDer druckfertige Export, gebaut durch Einbetten des PNGs
DOMDocument Object ModelDie Live-Repräsentation der Seite im Browser – das, was html-to-image in v2 rastert
HTML / CSSHyperText Markup Language / Cascading Style SheetsWoraus die Knoten in v2 bestehen – und warum ihr Text automatisch umbricht
APIApplication Programming InterfaceWie in File System Access API, die echte “Speichern unter…"-Dialoge bereitstellt
UI / UXUser Interface / User ExperienceDie Anforderung, einen Knoten ziehen zu können, die das Rewrite ausgelöst hat
JSONJavaScript Object NotationDie Baumstruktur, die Gemma für jede Mindmap generiert (siehe vorheriger Beitrag)
SQLite(SQL = Structured Query Language)Die Single-File-Datenbank, in der Layout-Entscheidungen und Knotenpositionen persistieren

Als Nächstes: Gamifying learning — 25 badges, idle-gap sessions, and a 90-day heatmap.

Ndimofor Aretas
Autoren
IT-Ausbilder & Full-Stack-Entwickler
Ein erfahrener Entwickler und zertifizierter IT-Ausbilder (IHK) mit Sitz in Deutschland, der leidenschaftlich gerne Wissen teilt und komplexe technische Konzepte durch praxisorientierte technische Inhalte und Projekte zugänglich macht.