diff --git a/static/js/modules/mindmap.js b/static/js/modules/mindmap.js
index 0dfc9db..bf8613b 100644
--- a/static/js/modules/mindmap.js
+++ b/static/js/modules/mindmap.js
@@ -1,844 +1,546 @@
/**
- * MindMap D3.js Modul
- * Visualisiert die Mindmap mit D3.js
+ * Mindmap-Visualisierungsmodul mit Cytoscape.js
+ * Erstellt eine interaktive Mindmap-Visualisierung
*/
-class MindMapVisualization {
- constructor(containerSelector, options = {}) {
- this.containerSelector = containerSelector;
- this.container = d3.select(containerSelector);
- this.width = options.width || this.container.node().clientWidth || 800;
- this.height = options.height || 600;
- this.nodeRadius = options.nodeRadius || 14;
- this.selectedNodeRadius = options.selectedNodeRadius || 20;
- this.linkDistance = options.linkDistance || 150;
- this.chargeStrength = options.chargeStrength || -900;
- this.centerForce = options.centerForce || 0.15;
- this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
+// Globales MindMap-Objekt erstellen, falls es noch nicht existiert
+if (!window.MindMap) {
+ window.MindMap = {};
+}
- this.nodes = [];
- this.links = [];
- this.simulation = null;
- this.svg = null;
- this.linkElements = null;
- this.nodeElements = null;
- this.textElements = null;
- this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
+// MindMap Klasse hinzufügen
+window.MindMap.Visualization = class MindMapVisualization {
+ /**
+ * Konstruktor für die MindMap-Visualisierung
+ * @param {string} containerId - ID des HTML-Containers für die Visualisierung
+ * @param {object} options - Optionen für die Visualisierung
+ */
+ constructor(containerId, options = {}) {
+ this.containerId = containerId;
+ this.container = document.getElementById(containerId.replace('#', ''));
- this.mouseoverNode = null;
- this.selectedNode = null;
+ // Optionen mit Standardwerten
+ this.options = {
+ layout: options.layout || 'cose',
+ darkMode: options.darkMode || document.documentElement.classList.contains('dark'),
+ nodeClickCallback: options.nodeClickCallback || null,
+ height: options.height || 600,
+ ...options
+ };
- this.zoomFactor = 1;
- this.tooltipDiv = null;
- this.isLoading = true;
+ // Cytoscape-Instanz
+ this.cy = null;
- // Lade die gemerkten Knoten
- this.bookmarkedNodes = this.loadBookmarkedNodes();
-
- // Sicherstellen, dass der Container bereit ist
- if (this.container.node()) {
- this.init();
- this.setupDefaultNodes();
-
- // Sofortige Datenladung
- window.setTimeout(() => {
- this.loadData();
- }, 100);
- } else {
- console.error('Mindmap-Container nicht gefunden:', containerSelector);
- }
- }
-
- // Standardknoten als Fallback einrichten, falls die API nicht reagiert
- setupDefaultNodes() {
- // Basis-Mindmap mit Hauptthemen
- const defaultNodes = [
- { id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
- { id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
- { id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
- { id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
- { id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
- ];
-
- const defaultLinks = [
- { source: "root", target: "philosophy" },
- { source: "root", target: "science" },
- { source: "root", target: "technology" },
- { source: "root", target: "arts" }
- ];
-
- // Als Fallback verwenden, falls die API fehlschlägt
- this.defaultNodes = defaultNodes;
- this.defaultLinks = defaultLinks;
- }
-
- init() {
- // SVG erstellen, wenn noch nicht vorhanden
- if (!this.svg) {
- // Container zuerst leeren
- this.container.html('');
-
- this.svg = this.container
- .append('svg')
- .attr('width', '100%')
- .attr('height', this.height)
- .attr('viewBox', `0 0 ${this.width} ${this.height}`)
- .attr('class', 'mindmap-svg')
- .call(
- d3.zoom()
- .scaleExtent([0.1, 5])
- .on('zoom', (event) => {
- this.handleZoom(event.transform);
- })
- );
-
- // Hauptgruppe für alles, was zoom-transformierbar ist
- this.g = this.svg.append('g');
-
- // Tooltip initialisieren
- if (!d3.select('body').select('.node-tooltip').size()) {
- this.tooltipDiv = d3.select('body')
- .append('div')
- .attr('class', 'node-tooltip')
- .style('opacity', 0)
- .style('position', 'absolute')
- .style('pointer-events', 'none')
- .style('background', 'rgba(20, 20, 40, 0.9)')
- .style('color', '#ffffff')
- .style('border', '1px solid rgba(160, 80, 255, 0.2)')
- .style('border-radius', '6px')
- .style('padding', '8px 12px')
- .style('font-size', '14px')
- .style('max-width', '250px')
- .style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
- } else {
- this.tooltipDiv = d3.select('body').select('.node-tooltip');
- }
- }
-
- // Force-Simulation initialisieren
- this.simulation = d3.forceSimulation()
- .force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
- .force('charge', d3.forceManyBody().strength(this.chargeStrength))
- .force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
- .force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
-
- // Globale Mindmap-Instanz für externe Zugriffe setzen
- window.mindmapInstance = this;
- }
-
- handleZoom(transform) {
- this.g.attr('transform', transform);
- this.zoomFactor = transform.k;
-
- // Knotengröße anpassen, um bei Zoom lesbar zu bleiben
- if (this.nodeElements) {
- this.nodeElements
- .attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
- }
-
- // Textgröße anpassen
- if (this.textElements) {
- this.textElements
- .style('font-size', `${12 / Math.sqrt(transform.k)}px`);
- }
- }
-
- async loadData() {
- try {
- // Ladeindikator anzeigen
- this.showLoading();
-
- // Verwende sofort die Standarddaten für eine schnelle erste Anzeige
- this.nodes = [...this.defaultNodes];
- this.links = [...this.defaultLinks];
-
- // Visualisierung sofort aktualisieren
- this.isLoading = false;
- this.updateVisualization();
-
- // Status auf bereit setzen - don't wait for API
- this.container.attr('data-status', 'ready');
-
- // API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout
-
- const response = await fetch('/api/mindmap', {
- signal: controller.signal,
- headers: {
- 'Cache-Control': 'no-cache',
- 'Pragma': 'no-cache'
- }
- });
- clearTimeout(timeoutId);
-
- if (!response.ok) {
- console.warn(`HTTP Fehler: ${response.status}, versuche erneute Verbindung`);
-
- // Bei Verbindungsfehler versuchen, die Verbindung neu herzustellen
- const retryResponse = await fetch('/api/refresh-mindmap', {
- headers: {
- 'Cache-Control': 'no-cache',
- 'Pragma': 'no-cache'
- }
- });
-
- if (!retryResponse.ok) {
- throw new Error(`Retry failed with status: ${retryResponse.status}`);
- }
-
- const retryData = await retryResponse.json();
-
- if (!retryData.success || !retryData.nodes || retryData.nodes.length === 0) {
- console.warn('Keine Mindmap-Daten nach Neuversuch, verwende weiterhin Standard-Daten.');
- return; // Keep using default data
- }
-
- // Flache Liste von Knoten und Verbindungen erstellen
- this.nodes = [];
- this.links = [];
-
- // Knoten direkt übernehmen
- retryData.nodes.forEach(node => {
- this.nodes.push({
- id: node.id,
- name: node.name,
- description: node.description || '',
- thought_count: node.thought_count || 0,
- color: this.generateColorFromString(node.name),
- });
-
- // Verbindungen hinzufügen
- if (node.connections && node.connections.length > 0) {
- node.connections.forEach(conn => {
- this.links.push({
- source: node.id,
- target: conn.target
- });
- });
- }
- });
-
- // Visualisierung aktualisieren mit den tatsächlichen Daten
- this.updateVisualization();
- return;
- }
-
- const data = await response.json();
-
- if (!data || !data.nodes || data.nodes.length === 0) {
- console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
- return; // Keep using default data
- }
-
- // Flache Liste von Knoten und Verbindungen erstellen
- this.nodes = [];
- this.links = [];
- this.processHierarchicalData(data.nodes);
-
- // Visualisierung aktualisieren mit den tatsächlichen Daten
- this.updateVisualization();
-
- } catch (error) {
- console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
- // Already using default data, no action needed
- }
-
- } catch (error) {
- console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
- this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
- this.container.attr('data-status', 'error');
- }
- }
-
- showLoading() {
- // Element nur leeren, wenn es noch kein SVG enthält
- if (!this.container.select('svg').size()) {
- this.container.html(`
-
-
-
-
Mindmap wird geladen...
-
-
- `);
- }
- }
-
- processHierarchicalData(hierarchicalNodes, parentId = null) {
- hierarchicalNodes.forEach(node => {
- // Knoten hinzufügen, wenn noch nicht vorhanden
- if (!this.nodes.find(n => n.id === node.id)) {
- this.nodes.push({
- id: node.id,
- name: node.name,
- description: node.description || '',
- thought_count: node.thought_count || 0,
- color: this.generateColorFromString(node.name),
- });
- }
-
- // Verbindung zum Elternknoten hinzufügen
- if (parentId !== null) {
- this.links.push({
- source: parentId,
- target: node.id
- });
- }
-
- // Rekursiv für Kindknoten aufrufen
- if (node.children && node.children.length > 0) {
- this.processHierarchicalData(node.children, node.id);
- }
- });
- }
-
- generateColorFromString(str) {
- // Erzeugt eine deterministische Farbe basierend auf dem String
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- }
-
- // Verwende deterministische Farbe aus unserem Farbschema
- const colors = [
- '#4080ff', // primary-400
- '#a040ff', // secondary-400
- '#205cf5', // primary-500
- '#8020f5', // secondary-500
- '#1040e0', // primary-600
- '#6010e0', // secondary-600
- ];
-
- return colors[Math.abs(hash) % colors.length];
- }
-
- updateVisualization() {
- // Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
- if (this.isLoading) return;
-
- // Container leeren, wenn Diagramm neu erstellt wird
- if (!this.svg) {
- this.container.html('');
- this.init();
- }
-
- // Performance-Optimierung: Deaktiviere Transition während des Datenladens
- const useTransitions = false;
-
- // Links (Edges) erstellen
- this.linkElements = this.g.selectAll('.link')
- .data(this.links)
- .join(
- enter => enter.append('line')
- .attr('class', 'link')
- .attr('stroke', '#ffffff30')
- .attr('stroke-width', 2)
- .attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
- .attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
- update => update
- .attr('stroke', '#ffffff30')
- .attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
- .attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
- exit => exit.remove()
- );
-
- // Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
- if (!this.svg.select('defs').node()) {
- const defs = this.svg.append('defs');
- defs.append('marker')
- .attr('id', 'arrowhead')
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 20)
- .attr('refY', 0)
- .attr('orient', 'auto')
- .attr('markerWidth', 6)
- .attr('markerHeight', 6)
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5')
- .attr('fill', '#ffffff50');
- }
-
- // Simplified Effekte definieren, falls noch nicht vorhanden
- if (!this.svg.select('#glow').node()) {
- const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
-
- // Glow-Effekt für Knoten
- const filter = defs.append('filter')
- .attr('id', 'glow')
- .attr('x', '-50%')
- .attr('y', '-50%')
- .attr('width', '200%')
- .attr('height', '200%');
-
- filter.append('feGaussianBlur')
- .attr('stdDeviation', '1')
- .attr('result', 'blur');
-
- filter.append('feComposite')
- .attr('in', 'SourceGraphic')
- .attr('in2', 'blur')
- .attr('operator', 'over');
-
- // Blur-Effekt für Schatten
- const blurFilter = defs.append('filter')
- .attr('id', 'blur')
- .attr('x', '-50%')
- .attr('y', '-50%')
- .attr('width', '200%')
- .attr('height', '200%');
-
- blurFilter.append('feGaussianBlur')
- .attr('stdDeviation', '1');
- }
-
- // Knoten-Gruppe erstellen/aktualisieren
- const nodeGroups = this.g.selectAll('.node-group')
- .data(this.nodes)
- .join(
- enter => {
- const group = enter.append('g')
- .attr('class', 'node-group')
- .call(d3.drag()
- .on('start', (event, d) => this.dragStarted(event, d))
- .on('drag', (event, d) => this.dragged(event, d))
- .on('end', (event, d) => this.dragEnded(event, d)));
-
- // Hintergrundschatten für besseren Kontrast
- group.append('circle')
- .attr('class', 'node-shadow')
- .attr('r', d => this.nodeRadius * 1.2)
- .attr('fill', 'rgba(0, 0, 0, 0.3)')
- .attr('filter', 'url(#blur)');
-
- // Kreis für jeden Knoten
- group.append('circle')
- .attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
- .attr('r', this.nodeRadius)
- .attr('fill', d => d.color || this.generateColorFromString(d.name))
- .attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
- .attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
- .attr('filter', 'url(#glow)');
-
- // Text-Label mit besserem Kontrast
- group.append('text')
- .attr('class', 'node-label')
- .attr('dy', '0.35em')
- .attr('text-anchor', 'middle')
- .attr('fill', '#ffffff')
- .attr('stroke', 'rgba(0, 0, 0, 0.4)')
- .attr('stroke-width', '0.7px')
- .attr('paint-order', 'stroke')
- .style('font-size', '12px')
- .style('font-weight', '500')
- .style('pointer-events', 'none')
- .text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
-
- // Interaktivität hinzufügen
- group
- .on('mouseover', (event, d) => this.nodeMouseover(event, d))
- .on('mouseout', (event, d) => this.nodeMouseout(event, d))
- .on('click', (event, d) => this.nodeClicked(event, d));
-
- return group;
- },
- update => {
- // Knoten aktualisieren
- update.select('.node')
- .attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
- .attr('fill', d => d.color || this.generateColorFromString(d.name))
- .attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
- .attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
-
- // Text aktualisieren
- update.select('.node-label')
- .text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
-
- return update;
- },
- exit => exit.remove()
- );
-
- // Einzelne Elemente für direkten Zugriff speichern
- this.nodeElements = this.g.selectAll('.node');
- this.textElements = this.g.selectAll('.node-label');
-
- // Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
- this.simulation
- .nodes(this.nodes)
- .on('tick', () => this.ticked())
- .alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
- .alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
-
- this.simulation.force('link')
- .links(this.links);
-
- // Simulation neu starten
- this.simulation.restart();
-
- // Update connection counts
- this.updateConnectionCounts();
- }
-
- ticked() {
- // Linienpositionen aktualisieren
- this.linkElements
- .attr('x1', d => d.source.x)
- .attr('y1', d => d.source.y)
- .attr('x2', d => d.target.x)
- .attr('y2', d => d.target.y);
-
- // Knotenpositionen aktualisieren
- this.g.selectAll('.node-group')
- .attr('transform', d => `translate(${d.x}, ${d.y})`);
- }
-
- dragStarted(event, d) {
- if (!event.active) this.simulation.alphaTarget(0.3).restart();
- d.fx = d.x;
- d.fy = d.y;
- }
-
- dragged(event, d) {
- d.fx = event.x;
- d.fy = event.y;
- }
-
- dragEnded(event, d) {
- if (!event.active) this.simulation.alphaTarget(0);
- d.fx = null;
- d.fy = null;
- }
-
- nodeMouseover(event, d) {
- this.mouseoverNode = d;
-
- // Tooltip anzeigen
- if (this.tooltipEnabled) {
- const isBookmarked = this.isNodeBookmarked(d.id);
- const tooltipContent = `
-
-
${d.name}
- ${d.description ? `
${d.description}
` : ''}
-
- Gedanken: ${d.thought_count}
-
-
-
-
-
- `;
-
- this.tooltipDiv
- .html(tooltipContent)
- .style('left', (event.pageX + 10) + 'px')
- .style('top', (event.pageY - 10) + 'px')
- .transition()
- .duration(200)
- .style('opacity', 1);
-
- // Event-Listener für den Bookmark-Button hinzufügen
- document.getElementById('bookmark-button').addEventListener('click', (e) => {
- e.stopPropagation();
- const nodeId = e.currentTarget.getAttribute('data-nodeid');
- const isNowBookmarked = this.toggleBookmark(nodeId);
-
- // Button-Text aktualisieren
- if (isNowBookmarked) {
- e.currentTarget.innerHTML = ' Gemerkt';
- } else {
- e.currentTarget.innerHTML = ' Merken';
- }
- });
- }
-
- // Knoten visuell hervorheben
- d3.select(event.currentTarget).select('circle')
- .transition()
- .duration(200)
- .attr('r', this.nodeRadius * 1.2)
- .attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
- }
-
- nodeMouseout(event, d) {
- this.mouseoverNode = null;
-
- // Tooltip ausblenden
- if (this.tooltipEnabled) {
- this.tooltipDiv
- .transition()
- .duration(200)
- .style('opacity', 0);
- }
-
- // Knoten-Stil zurücksetzen, wenn nicht ausgewählt
- const nodeElement = d3.select(event.currentTarget).select('circle');
- if (d !== this.selectedNode) {
- const isBookmarked = this.isNodeBookmarked(d.id);
- nodeElement
- .transition()
- .duration(200)
- .attr('r', this.nodeRadius)
- .attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
- .attr('stroke-width', isBookmarked ? 3 : 2);
- }
- }
-
- nodeClicked(event, d) {
- // Frühere Auswahl zurücksetzen
- if (this.selectedNode && this.selectedNode !== d) {
- this.g.selectAll('.node')
- .filter(n => n === this.selectedNode)
- .transition()
- .duration(200)
- .attr('r', this.nodeRadius)
- .attr('stroke', '#ffffff50');
- }
-
- // Neue Auswahl hervorheben
- if (this.selectedNode !== d) {
- this.selectedNode = d;
- d3.select(event.currentTarget).select('circle')
- .transition()
- .duration(200)
- .attr('r', this.selectedNodeRadius)
- .attr('stroke', '#ffffff');
- }
-
- // Callback mit Node-Daten aufrufen
- this.onNodeClick(d);
- }
-
- showError(message) {
- this.container.html(`
-
- `);
- }
-
- // Fokussiert die Ansicht auf einen bestimmten Knoten
- focusNode(nodeId) {
- const node = this.nodes.find(n => n.id === nodeId);
- if (!node) return;
-
- // Simuliere einen Klick auf den Knoten
- const nodeElement = this.g.selectAll('.node-group')
- .filter(d => d.id === nodeId);
-
- nodeElement.dispatch('click');
-
- // Zentriere den Knoten in der Ansicht
- const transform = d3.zoomIdentity
- .translate(this.width / 2, this.height / 2)
- .scale(1.2)
- .translate(-node.x, -node.y);
-
- this.svg.transition()
- .duration(750)
- .call(
- d3.zoom().transform,
- transform
- );
- }
-
- // Filtert die Mindmap basierend auf einem Suchbegriff
- filterBySearchTerm(searchTerm) {
- if (!searchTerm || searchTerm.trim() === '') {
- // Alle Knoten anzeigen
- this.g.selectAll('.node-group')
- .style('opacity', 1)
- .style('pointer-events', 'all');
-
- this.g.selectAll('.link')
- .style('opacity', 1);
-
+ // Vorbereitende Prüfungen
+ if (!this.container) {
+ console.error(`Container mit ID ${containerId} nicht gefunden`);
return;
}
- const searchLower = searchTerm.toLowerCase();
- const matchingNodes = this.nodes.filter(node =>
- node.name.toLowerCase().includes(searchLower) ||
- (node.description && node.description.toLowerCase().includes(searchLower))
- );
-
- const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
-
- // Passende Knoten hervorheben, andere ausblenden
- this.g.selectAll('.node-group')
- .style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
- .style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
-
- // Verbindungen zwischen passenden Knoten hervorheben
- this.g.selectAll('.link')
- .style('opacity', d =>
- matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
- );
-
- // Auf den ersten passenden Knoten fokussieren, wenn vorhanden
- if (matchingNodes.length > 0) {
- this.focusNode(matchingNodes[0].id);
- }
- }
-
- /**
- * Updates the thought_count property for each node based on existing connections
- */
- updateConnectionCounts() {
- // Reset all counts first
- this.nodes.forEach(node => {
- // Initialize thought_count if it doesn't exist
- if (typeof node.thought_count !== 'number') {
- node.thought_count = 0;
- }
-
- // Count connections for this node
- const connectedNodes = this.getConnectedNodes(node);
- node.thought_count = connectedNodes.length;
- });
-
- // Update UI to show counts
- this.updateNodeLabels();
- }
-
- /**
- * Updates the visual representation of node labels to include connection counts
- */
- updateNodeLabels() {
- if (!this.textElements) return;
-
- this.textElements.text(d => {
- if (d.thought_count > 0) {
- return `${d.name} (${d.thought_count})`;
- }
- return d.name;
- });
- }
-
- /**
- * Adds a new connection between nodes and updates the counts
- */
- addConnection(sourceNode, targetNode) {
- if (!sourceNode || !targetNode) return false;
-
- // Check if connection already exists
- if (this.isConnected(sourceNode, targetNode)) return false;
-
- // Add new connection
- this.links.push({
- source: sourceNode.id,
- target: targetNode.id
- });
-
- // Update counts
- this.updateConnectionCounts();
-
- // Update visualization
- this.updateVisualization();
-
- return true;
- }
-
- // Lädt gemerkete Knoten aus dem LocalStorage
- loadBookmarkedNodes() {
- try {
- const bookmarked = localStorage.getItem('bookmarkedNodes');
- return bookmarked ? JSON.parse(bookmarked) : [];
- } catch (error) {
- console.error('Fehler beim Laden der gemerkten Knoten:', error);
- return [];
- }
- }
-
- // Speichert gemerkete Knoten im LocalStorage
- saveBookmarkedNodes() {
- try {
- localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
- } catch (error) {
- console.error('Fehler beim Speichern der gemerkten Knoten:', error);
- }
- }
-
- // Prüft, ob ein Knoten gemerkt ist
- isNodeBookmarked(nodeId) {
- return this.bookmarkedNodes.includes(nodeId);
- }
-
- // Merkt einen Knoten oder hebt die Markierung auf
- toggleBookmark(nodeId) {
- const index = this.bookmarkedNodes.indexOf(nodeId);
- if (index === -1) {
- // Node hinzufügen
- this.bookmarkedNodes.push(nodeId);
- this.updateNodeAppearance(nodeId, true);
+ // Cytoscape-Bibliothek prüfen
+ if (typeof cytoscape === 'undefined') {
+ this._loadCytoscapeLibrary()
+ .then(() => this.initialize())
+ .catch(error => {
+ console.error('Cytoscape.js konnte nicht geladen werden:', error);
+ this._showError('Cytoscape.js konnte nicht geladen werden. Bitte laden Sie die Seite neu.');
+ });
} else {
- // Node entfernen
- this.bookmarkedNodes.splice(index, 1);
- this.updateNodeAppearance(nodeId, false);
+ this.initialize();
}
+ }
+
+ /**
+ * Lädt die Cytoscape-Bibliothek dynamisch
+ * @returns {Promise} Promise, das nach dem Laden erfüllt wird
+ */
+ _loadCytoscapeLibrary() {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js';
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ }
+
+ /**
+ * Initialisiert die Cytoscape-Instanz
+ */
+ initialize() {
+ // Container-Dimensionen festlegen
+ this.container.style.height = `${this.options.height}px`;
- // Änderungen speichern
- this.saveBookmarkedNodes();
+ // Cytoscape-Instanz erstellen
+ this.cy = cytoscape({
+ container: this.container,
+ elements: [],
+ style: this._createStylesheet(),
+ layout: this._getLayoutOptions(),
+ wheelSensitivity: 0.2,
+ minZoom: 0.1,
+ maxZoom: 3,
+ boxSelectionEnabled: false
+ });
- // Event auslösen für andere Komponenten
- const event = new CustomEvent('nodeBookmarkToggled', {
- detail: {
- nodeId: nodeId,
- isBookmarked: index === -1
+ // Event-Listener für Knoten-Klicks
+ this.cy.on('tap', 'node', event => {
+ const node = event.target;
+
+ // Alle Knoten zurücksetzen
+ this.cy.nodes().removeClass('selected');
+
+ // Ausgewählten Knoten markieren
+ node.addClass('selected');
+
+ // Callback aufrufen, falls definiert
+ if (this.options.nodeClickCallback) {
+ this.options.nodeClickCallback(node.data());
}
});
- document.dispatchEvent(event);
- return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
+ // Event-Listener für Hintergrund-Klicks (Selektion aufheben)
+ this.cy.on('tap', event => {
+ if (event.target === this.cy) {
+ this.cy.nodes().removeClass('selected');
+ }
+ });
+
+ // Dark Mode Änderungen überwachen
+ document.addEventListener('darkModeToggled', event => {
+ this.updateDarkMode(event.detail.isDark);
+ });
}
- // Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
- updateNodeAppearance(nodeId, isBookmarked) {
- this.g.selectAll('.node-group')
- .filter(d => d.id === nodeId)
- .select('.node')
- .classed('bookmarked', isBookmarked)
- .attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
- .attr('stroke-width', isBookmarked ? 3 : 2);
+ /**
+ * Erstellt das Stylesheet für Cytoscape
+ * @returns {Array} Array mit Stilregeln
+ */
+ _createStylesheet() {
+ const isDarkMode = this.options.darkMode;
+
+ return [
+ {
+ selector: 'node',
+ style: {
+ 'background-color': 'data(color)',
+ 'label': 'data(name)',
+ 'width': 30,
+ 'height': 30,
+ 'font-size': 12,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 8,
+ 'color': isDarkMode ? '#f1f5f9' : '#334155',
+ 'text-background-color': isDarkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)',
+ 'text-background-opacity': 0.8,
+ 'text-background-padding': '2px',
+ 'text-background-shape': 'roundrectangle',
+ 'text-wrap': 'ellipsis',
+ 'text-max-width': '100px'
+ }
+ },
+ {
+ selector: 'edge',
+ style: {
+ 'width': 2,
+ 'line-color': isDarkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
+ 'target-arrow-color': isDarkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
+ 'curve-style': 'bezier'
+ }
+ },
+ {
+ selector: 'node.selected',
+ style: {
+ 'background-color': 'data(color)',
+ 'border-width': 3,
+ 'border-color': '#8b5cf6',
+ 'width': 40,
+ 'height': 40,
+ 'font-size': 14,
+ 'font-weight': 'bold',
+ 'text-background-color': '#8b5cf6',
+ 'text-background-opacity': 0.9
+ }
+ }
+ ];
}
- // Aktualisiert das Aussehen aller gemerkten Knoten
- updateAllBookmarkedNodes() {
- this.g.selectAll('.node-group')
- .each((d) => {
- const isBookmarked = this.isNodeBookmarked(d.id);
- this.updateNodeAppearance(d.id, isBookmarked);
+ /**
+ * Ermittelt die Layout-Optionen basierend auf dem gewählten Layout
+ * @returns {object} Layout-Optionen für Cytoscape
+ */
+ _getLayoutOptions() {
+ const layouts = {
+ 'cose': {
+ name: 'cose',
+ animate: true,
+ animationDuration: 800,
+ nodeDimensionsIncludeLabels: true,
+ refresh: 30,
+ randomize: true,
+ componentSpacing: 100,
+ nodeRepulsion: 8000,
+ nodeOverlap: 20,
+ idealEdgeLength: 200,
+ edgeElasticity: 100,
+ nestingFactor: 1.2,
+ gravity: 80,
+ fit: true,
+ padding: 30
+ },
+ 'circle': {
+ name: 'circle',
+ fit: true,
+ padding: 50,
+ radius: 200,
+ startAngle: 3 / 2 * Math.PI,
+ sweep: 2 * Math.PI,
+ clockwise: true,
+ sort: (a, b) => a.data('name').localeCompare(b.data('name'))
+ },
+ 'concentric': {
+ name: 'concentric',
+ fit: true,
+ padding: 50,
+ concentric: node => node.data('connections') || 1,
+ levelWidth: () => 1,
+ minNodeSpacing: 50
+ }
+ };
+
+ return layouts[this.options.layout] || layouts['cose'];
+ }
+
+ /**
+ * Zeigt eine Fehlermeldung im Container an
+ * @param {string} message - Die anzuzeigende Fehlermeldung
+ */
+ _showError(message) {
+ this.container.innerHTML = `
+
+
+
${message}
+
Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.
+
+ `;
+ }
+
+ /**
+ * Zeigt eine Ladeanimation im Container an
+ */
+ showLoading() {
+ this.container.innerHTML = `
+
+ `;
+ }
+
+ /**
+ * Lädt Daten aus der API und rendert sie
+ */
+ loadData() {
+ // Ladeanimation anzeigen
+ this.showLoading();
+
+ // Versuche, Daten vom Server zu laden
+ fetch('/api/mindmap')
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Netzwerkfehler beim Laden der Mindmap-Daten');
+ }
+ return response.json();
+ })
+ .then(data => {
+ this.renderData(data);
+ })
+ .catch(error => {
+ console.error('Fehler beim Laden der Mindmap-Daten:', error);
+
+ // Verwende Standarddaten als Fallback
+ console.log('Verwende Standarddaten als Fallback...');
+ const defaultData = this._generateDefaultData();
+ this.renderData(defaultData);
});
}
-
+
/**
- * Gibt alle direkt verbundenen Knoten eines Knotens zurück
- * @param {Object} node - Der Knoten, für den die Verbindungen gesucht werden
- * @returns {Array} Array der verbundenen Knotenobjekte
+ * Generiert Standarddaten für die Mindmap als Fallback
+ * @returns {object} Standarddaten für die Mindmap
*/
- getConnectedNodes(node) {
- if (!node || !this.links || !this.nodes) return [];
- const nodeId = node.id;
- const connectedIds = new Set();
- this.links.forEach(link => {
- if (link.source === nodeId || (link.source && link.source.id === nodeId)) {
- connectedIds.add(link.target.id ? link.target.id : link.target);
+ _generateDefaultData() {
+ return {
+ nodes: [
+ { id: 'root', name: 'Wissen', description: 'Zentrale Wissensbasis', category: 'Zentral', color_code: '#4299E1' },
+ { id: 'philosophy', name: 'Philosophie', description: 'Philosophisches Denken', category: 'Philosophie', color_code: '#9F7AEA', parent_id: 'root' },
+ { id: 'science', name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', category: 'Wissenschaft', color_code: '#48BB78', parent_id: 'root' },
+ { id: 'technology', name: 'Technologie', description: 'Technologische Entwicklungen', category: 'Technologie', color_code: '#ED8936', parent_id: 'root' },
+ { id: 'arts', name: 'Künste', description: 'Künstlerische Ausdrucksformen', category: 'Künste', color_code: '#ED64A6', parent_id: 'root' },
+ { id: 'psychology', name: 'Psychologie', description: 'Menschliches Verhalten und Geist', category: 'Psychologie', color_code: '#4299E1', parent_id: 'root' }
+ ]
+ };
+ }
+
+ /**
+ * Rendert die Daten in der Cytoscape-Instanz
+ * @param {object} data - Daten für die Mindmap
+ */
+ renderData(data) {
+ if (!this.cy) {
+ console.error('Cytoscape-Instanz nicht initialisiert');
+ return;
+ }
+
+ // Konvertiere die Daten in das Cytoscape-Format
+ const elements = this._convertToCytoscapeFormat(data);
+
+ // Aktualisiere die Elemente
+ this.cy.elements().remove();
+ this.cy.add(elements);
+
+ // Layout neu berechnen und anwenden
+ const layout = this.cy.layout(this._getLayoutOptions());
+ layout.run();
+
+ // Zentriere und passe die Ansicht an
+ setTimeout(() => {
+ this.fitView();
+ }, 100);
+ }
+
+ /**
+ * Konvertiert die Daten in das Cytoscape-Format
+ * @param {object} data - Daten für die Mindmap
+ * @returns {Array} Cytoscape-Elemente
+ */
+ _convertToCytoscapeFormat(data) {
+ const elements = [];
+
+ // Knoten hinzufügen
+ if (data.nodes && data.nodes.length > 0) {
+ data.nodes.forEach(node => {
+ elements.push({
+ group: 'nodes',
+ data: {
+ id: String(node.id),
+ name: node.name,
+ description: node.description || 'Keine Beschreibung verfügbar',
+ category: node.category || 'Allgemein',
+ color: node.color_code || this._getRandomColor(),
+ connections: 0
+ }
+ });
+
+ // Kante zum Elternknoten hinzufügen (falls vorhanden)
+ if (node.parent_id) {
+ elements.push({
+ group: 'edges',
+ data: {
+ id: `edge-${node.parent_id}-${node.id}`,
+ source: String(node.parent_id),
+ target: String(node.id)
+ }
+ });
+ }
+ });
+
+ // Zusätzliche Kanten zwischen Knoten hinzufügen (falls in den Daten vorhanden)
+ if (data.edges && data.edges.length > 0) {
+ data.edges.forEach(edge => {
+ elements.push({
+ group: 'edges',
+ data: {
+ id: `edge-${edge.source}-${edge.target}`,
+ source: String(edge.source),
+ target: String(edge.target)
+ }
+ });
+ });
}
- if (link.target === nodeId || (link.target && link.target.id === nodeId)) {
- connectedIds.add(link.source.id ? link.source.id : link.source);
+ }
+
+ return elements;
+ }
+
+ /**
+ * Generiert eine zufällige Farbe
+ * @returns {string} Zufällige Farbe als HEX-Code
+ */
+ _getRandomColor() {
+ const colors = [
+ '#4299E1', // Blau
+ '#9F7AEA', // Lila
+ '#48BB78', // Grün
+ '#ED8936', // Orange
+ '#ED64A6', // Pink
+ '#F56565' // Rot
+ ];
+ return colors[Math.floor(Math.random() * colors.length)];
+ }
+
+ /**
+ * Aktualisiert den Dark Mode für die Visualisierung
+ * @param {boolean} isDark - Ob der Dark Mode aktiviert ist
+ */
+ updateDarkMode(isDark) {
+ if (!this.cy) return;
+
+ this.options.darkMode = isDark;
+
+ // Farben aktualisieren
+ const textColor = isDark ? '#f1f5f9' : '#334155';
+ const textBgColor = isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)';
+ const edgeColor = isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)';
+
+ // Stile aktualisieren
+ this.cy.style()
+ .selector('node')
+ .style({
+ 'color': textColor,
+ 'text-background-color': textBgColor
+ })
+ .selector('edge')
+ .style({
+ 'line-color': edgeColor,
+ 'target-arrow-color': edgeColor
+ })
+ .update();
+ }
+
+ /**
+ * Passt die Ansicht an die Mindmap an
+ */
+ fitView() {
+ if (!this.cy) return;
+
+ this.cy.fit();
+ this.cy.center();
+ }
+
+ /**
+ * Zoomt in die Mindmap hinein
+ */
+ zoomIn() {
+ if (!this.cy) return;
+
+ const zoom = this.cy.zoom() * 1.2;
+ this.cy.zoom({
+ level: zoom,
+ renderedPosition: { x: this.cy.width() / 2, y: this.cy.height() / 2 }
+ });
+ }
+
+ /**
+ * Zoomt aus der Mindmap heraus
+ */
+ zoomOut() {
+ if (!this.cy) return;
+
+ const zoom = this.cy.zoom() / 1.2;
+ this.cy.zoom({
+ level: zoom,
+ renderedPosition: { x: this.cy.width() / 2, y: this.cy.height() / 2 }
+ });
+ }
+
+ /**
+ * Setzt den Zoom zurück
+ */
+ resetZoom() {
+ if (!this.cy) return;
+
+ this.cy.zoom(1);
+ this.cy.center();
+ }
+
+ /**
+ * Berechnet das Layout neu
+ */
+ relayout() {
+ if (!this.cy) return;
+
+ const layout = this.cy.layout(this._getLayoutOptions());
+ layout.run();
+ }
+
+ /**
+ * Ändert das Layout
+ * @param {string} layoutName - Name des Layouts
+ */
+ changeLayout(layoutName) {
+ if (!this.cy) return;
+
+ this.options.layout = layoutName;
+ const layout = this.cy.layout(this._getLayoutOptions());
+ layout.run();
+ }
+
+ /**
+ * Fokussiert einen Knoten
+ * @param {string} nodeId - ID des zu fokussierenden Knotens
+ */
+ focusNode(nodeId) {
+ if (!this.cy) return;
+
+ const node = this.cy.getElementById(nodeId);
+ if (node.length > 0) {
+ this.cy.nodes().removeClass('selected');
+ node.addClass('selected');
+ this.cy.center(node);
+
+ // Callback aufrufen, falls definiert
+ if (this.options.nodeClickCallback) {
+ this.options.nodeClickCallback(node.data());
+ }
+ }
+ }
+
+ /**
+ * Sucht nach Knoten basierend auf einem Suchbegriff
+ * @param {string} searchTerm - Suchbegriff
+ */
+ search(searchTerm) {
+ if (!this.cy || !searchTerm) return;
+
+ const term = searchTerm.toLowerCase();
+ let found = false;
+
+ // Alle Knoten durchsuchen
+ this.cy.nodes().forEach(node => {
+ const name = node.data('name').toLowerCase();
+ const category = node.data('category').toLowerCase();
+ const description = node.data('description').toLowerCase();
+
+ // Prüfen, ob der Suchbegriff enthalten ist
+ if (name.includes(term) || category.includes(term) || description.includes(term)) {
+ found = true;
+ this.cy.nodes().removeClass('selected');
+ node.addClass('selected');
+ this.cy.center(node);
+
+ // Callback nur für den ersten gefundenen Knoten aufrufen
+ if (found && this.options.nodeClickCallback) {
+ this.options.nodeClickCallback(node.data());
+ }
+
+ return false; // Schleife abbrechen nach dem ersten Fund
}
});
- return this.nodes.filter(n => connectedIds.has(n.id));
}
-}
-
-// Exportiere die Klasse für die Verwendung in anderen Modulen
-window.MindMapVisualization = MindMapVisualization;
\ No newline at end of file
+
+ /**
+ * Gibt die Daten des ausgewählten Knotens zurück
+ * @returns {object|null} Daten des ausgewählten Knotens oder null
+ */
+ getSelectedNodeData() {
+ if (!this.cy) return null;
+
+ const selectedNodes = this.cy.nodes('.selected');
+ if (selectedNodes.length > 0) {
+ return selectedNodes[0].data();
+ }
+
+ return null;
+ }
+};
\ No newline at end of file