Files
website/static/js/modules/mindmap.js

546 lines
15 KiB
JavaScript

/**
* Mindmap-Visualisierungsmodul mit Cytoscape.js
* Erstellt eine interaktive Mindmap-Visualisierung
*/
// Globales MindMap-Objekt erstellen, falls es noch nicht existiert
if (!window.MindMap) {
window.MindMap = {};
}
// 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('#', ''));
// 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
};
// Cytoscape-Instanz
this.cy = null;
// Vorbereitende Prüfungen
if (!this.container) {
console.error(`Container mit ID ${containerId} nicht gefunden`);
return;
}
// 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 {
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`;
// 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-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());
}
});
// 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);
});
}
/**
* 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
}
}
];
}
/**
* 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 = `
<div class="p-8 text-center bg-red-50 dark:bg-red-900/20 rounded-lg">
<i class="fa-solid fa-triangle-exclamation text-4xl text-red-500 mb-4"></i>
<p class="text-lg text-red-600 dark:text-red-400">${message}</p>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.</p>
</div>
`;
}
/**
* Zeigt eine Ladeanimation im Container an
*/
showLoading() {
this.container.innerHTML = `
<div class="flex justify-center items-center h-full">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"></div>
</div>
`;
}
/**
* 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);
});
}
/**
* Generiert Standarddaten für die Mindmap als Fallback
* @returns {object} Standarddaten für die Mindmap
*/
_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)
}
});
});
}
}
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
}
});
}
/**
* 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;
}
};