✨ feat: implementiere Mindmap-Funktionalität mit dynamischer Datenladung und verbesserten Benutzeroberflächen-Elementen in mindmap.html und mindmap-init.js
This commit is contained in:
420
static/js/mindmap-init.js
Normal file
420
static/js/mindmap-init.js
Normal file
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* Mindmap-Initialisierer
|
||||
* Lädt und initialisiert die Mindmap-Visualisierung
|
||||
*/
|
||||
|
||||
// Warte bis DOM geladen ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||
const cyContainer = document.getElementById('cy');
|
||||
|
||||
if (!cyContainer) {
|
||||
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initialisiere Mindmap-Visualisierung...');
|
||||
|
||||
// Prüfe, ob Cytoscape.js verfügbar ist
|
||||
if (typeof cytoscape === 'undefined') {
|
||||
loadScript('/static/js/cytoscape.min.js', initMindmap);
|
||||
} else {
|
||||
initMindmap();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Lädt ein Script dynamisch
|
||||
* @param {string} src - Quelldatei
|
||||
* @param {Function} callback - Callback nach dem Laden
|
||||
*/
|
||||
function loadScript(src, callback) {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = callback;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert die Mindmap-Visualisierung
|
||||
*/
|
||||
function initMindmap() {
|
||||
const cyContainer = document.getElementById('cy');
|
||||
const fitBtn = document.getElementById('fit-btn');
|
||||
const resetBtn = document.getElementById('reset-btn');
|
||||
const toggleLabelsBtn = document.getElementById('toggle-labels-btn');
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
const nodeDescription = document.getElementById('node-description');
|
||||
const connectedNodes = document.getElementById('connected-nodes');
|
||||
|
||||
let labelsVisible = true;
|
||||
let selectedNode = null;
|
||||
|
||||
// Erstelle Cytoscape-Instanz
|
||||
const cy = cytoscape({
|
||||
container: cyContainer,
|
||||
style: getDefaultStyles(),
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 50,
|
||||
spacingFactor: 1.2,
|
||||
randomize: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 1.2,
|
||||
gravity: 80
|
||||
},
|
||||
wheelSensitivity: 0.3,
|
||||
});
|
||||
|
||||
// Daten vom Server laden
|
||||
loadMindmapData(cy);
|
||||
|
||||
// Event-Handler zuweisen
|
||||
setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Mindmap-Daten vom Server
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function loadMindmapData(cy) {
|
||||
fetch('/api/mindmap')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Fehler: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.nodes || data.nodes.length === 0) {
|
||||
console.log('Keine Daten gefunden, versuche Refresh-API...');
|
||||
return fetch('/api/refresh-mindmap')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Fehler beim Refresh: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Mindmap-Daten geladen:', data);
|
||||
|
||||
// Cytoscape-Elemente vorbereiten
|
||||
const elements = [];
|
||||
|
||||
// Prüfen, ob "Wissen"-Knoten existiert
|
||||
let rootNode = data.nodes.find(node => node.name === "Wissen");
|
||||
|
||||
// Wenn nicht, Root-Knoten hinzufügen
|
||||
if (!rootNode) {
|
||||
rootNode = {
|
||||
id: 'root',
|
||||
name: 'Wissen',
|
||||
description: 'Zentrale Wissensbasis',
|
||||
color_code: '#4299E1'
|
||||
};
|
||||
data.nodes.unshift(rootNode);
|
||||
}
|
||||
|
||||
// Knoten hinzufügen
|
||||
data.nodes.forEach(node => {
|
||||
elements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color: node.color_code || '#8B5CF6',
|
||||
isRoot: node.name === 'Wissen'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Kanten hinzufügen, wenn vorhanden
|
||||
if (data.edges && data.edges.length > 0) {
|
||||
data.edges.forEach(edge => {
|
||||
elements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${edge.source}-${edge.target}`,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString()
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Wenn keine Kanten definiert sind, verbinde alle Knoten mit dem Root-Knoten
|
||||
const rootId = rootNode.id.toString();
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
if (node.id.toString() !== rootId) {
|
||||
elements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${rootId}-${node.id}`,
|
||||
source: rootId,
|
||||
target: node.id.toString()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Elemente zu Cytoscape hinzufügen
|
||||
cy.elements().remove();
|
||||
cy.add(elements);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 50,
|
||||
spacingFactor: 1.5,
|
||||
randomize: false,
|
||||
fit: true
|
||||
}).run();
|
||||
|
||||
// Nach dem Laden Event auslösen
|
||||
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||
|
||||
// Fallback mit Standard-Daten
|
||||
const fallbackData = {
|
||||
nodes: [
|
||||
{ id: 1, name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' },
|
||||
{ id: 2, name: 'Philosophie', description: 'Philosophisches Denken', color_code: '#9F7AEA' },
|
||||
{ id: 3, name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', color_code: '#48BB78' },
|
||||
{ id: 4, name: 'Technologie', description: 'Technologische Entwicklungen', color_code: '#ED8936' },
|
||||
{ id: 5, name: 'Künste', description: 'Künstlerische Ausdrucksformen', color_code: '#ED64A6' }
|
||||
],
|
||||
edges: [
|
||||
{ source: 1, target: 2 },
|
||||
{ source: 1, target: 3 },
|
||||
{ source: 1, target: 4 },
|
||||
{ source: 1, target: 5 }
|
||||
]
|
||||
};
|
||||
|
||||
const fallbackElements = [];
|
||||
|
||||
// Knoten hinzufügen
|
||||
fallbackData.nodes.forEach(node => {
|
||||
fallbackElements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color: node.color_code || '#8B5CF6',
|
||||
isRoot: node.name === 'Wissen'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Kanten hinzufügen
|
||||
fallbackData.edges.forEach(edge => {
|
||||
fallbackElements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `${edge.source}-${edge.target}`,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Elemente zu Cytoscape hinzufügen
|
||||
cy.elements().remove();
|
||||
cy.add(fallbackElements);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({ name: 'cose', animate: true }).run();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Richtet Event-Listener für die Mindmap ein
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
* @param {HTMLElement} fitBtn - Fit-Button
|
||||
* @param {HTMLElement} resetBtn - Reset-Button
|
||||
* @param {HTMLElement} toggleLabelsBtn - Toggle-Labels-Button
|
||||
* @param {HTMLElement} nodeInfoPanel - Node-Info-Panel
|
||||
* @param {HTMLElement} nodeDescription - Node-Description
|
||||
* @param {HTMLElement} connectedNodes - Connected-Nodes-Container
|
||||
*/
|
||||
function setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes) {
|
||||
let labelsVisible = true;
|
||||
|
||||
// Fit-Button
|
||||
if (fitBtn) {
|
||||
fitBtn.addEventListener('click', function() {
|
||||
cy.fit();
|
||||
});
|
||||
}
|
||||
|
||||
// Reset-Button
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function() {
|
||||
cy.layout({ name: 'cose', animate: true }).run();
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle-Labels-Button
|
||||
if (toggleLabelsBtn) {
|
||||
toggleLabelsBtn.addEventListener('click', function() {
|
||||
labelsVisible = !labelsVisible;
|
||||
cy.style()
|
||||
.selector('node')
|
||||
.style({
|
||||
'text-opacity': labelsVisible ? 1 : 0
|
||||
})
|
||||
.update();
|
||||
});
|
||||
}
|
||||
|
||||
// Knoten-Klick
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
|
||||
// Zuvor ausgewählten Knoten zurücksetzen
|
||||
cy.nodes().removeClass('selected');
|
||||
|
||||
// Neuen Knoten auswählen
|
||||
node.addClass('selected');
|
||||
|
||||
if (nodeInfoPanel && nodeDescription && connectedNodes) {
|
||||
// Info-Panel aktualisieren
|
||||
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
|
||||
|
||||
// Verbundene Knoten anzeigen
|
||||
connectedNodes.innerHTML = '';
|
||||
|
||||
// Verbundene Knoten sammeln
|
||||
const connectedNodesList = node.neighborhood('node');
|
||||
|
||||
if (connectedNodesList.length > 0) {
|
||||
connectedNodesList.forEach(connectedNode => {
|
||||
// Nicht den ausgewählten Knoten selbst anzeigen
|
||||
if (connectedNode.id() !== node.id()) {
|
||||
const nodeLink = document.createElement('span');
|
||||
nodeLink.className = 'node-link';
|
||||
nodeLink.textContent = connectedNode.data('name');
|
||||
nodeLink.style.backgroundColor = connectedNode.data('color');
|
||||
|
||||
// Klick-Ereignis, um zu diesem Knoten zu wechseln
|
||||
nodeLink.addEventListener('click', function() {
|
||||
connectedNode.select();
|
||||
cy.animate({
|
||||
center: { eles: connectedNode },
|
||||
duration: 500,
|
||||
easing: 'ease-in-out-cubic'
|
||||
});
|
||||
});
|
||||
|
||||
connectedNodes.appendChild(nodeLink);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
connectedNodes.innerHTML = '<em>Keine verbundenen Knoten</em>';
|
||||
}
|
||||
|
||||
// Panel anzeigen
|
||||
nodeInfoPanel.classList.add('visible');
|
||||
}
|
||||
});
|
||||
|
||||
// Hintergrund-Klick
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
// Klick auf den Hintergrund
|
||||
cy.nodes().removeClass('selected');
|
||||
|
||||
// Info-Panel verstecken
|
||||
if (nodeInfoPanel) {
|
||||
nodeInfoPanel.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Dark Mode-Änderungen
|
||||
document.addEventListener('darkModeToggled', function(event) {
|
||||
const isDark = event.detail.isDark;
|
||||
cy.style(getDefaultStyles(isDark));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Standard-Stile für die Mindmap
|
||||
* @param {boolean} darkMode - Ob der Dark Mode aktiv ist
|
||||
* @returns {Array} Array von Cytoscape-Stilen
|
||||
*/
|
||||
function getDefaultStyles(darkMode = document.documentElement.classList.contains('dark')) {
|
||||
return [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': 'data(color)',
|
||||
'label': 'data(name)',
|
||||
'width': 40,
|
||||
'height': 40,
|
||||
'font-size': 12,
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'text-margin-y': 8,
|
||||
'color': darkMode ? '#f1f5f9' : '#334155',
|
||||
'text-background-color': darkMode ? '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: 'node[?isRoot]',
|
||||
style: {
|
||||
'width': 60,
|
||||
'height': 60,
|
||||
'font-size': 14,
|
||||
'font-weight': 'bold',
|
||||
'text-background-opacity': 0.9,
|
||||
'text-background-color': '#4299E1'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
'line-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
||||
'target-arrow-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
||||
'curve-style': 'bezier',
|
||||
'target-arrow-shape': 'triangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.selected',
|
||||
style: {
|
||||
'background-color': 'data(color)',
|
||||
'border-width': 3,
|
||||
'border-color': '#8b5cf6',
|
||||
'width': 50,
|
||||
'height': 50,
|
||||
'font-size': 14,
|
||||
'font-weight': 'bold',
|
||||
'text-background-color': '#8b5cf6',
|
||||
'text-background-opacity': 0.9
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user