feat(mindmap): enhance interaction and initialization logic in mindmap files
This commit is contained in:
@@ -1,648 +1,214 @@
|
||||
/**
|
||||
* Mindmap-Initialisierer
|
||||
* Lädt und initialisiert die Mindmap-Visualisierung
|
||||
* Mindmap Initialisierung und Event-Handling
|
||||
*/
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
// Warte auf die Cytoscape-Instanz
|
||||
document.addEventListener('mindmap-loaded', function() {
|
||||
const cy = window.cy;
|
||||
if (!cy) return;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
// Erstelle Cytoscape-Instanz
|
||||
const cy = cytoscape({
|
||||
container: cyContainer,
|
||||
style: getNeuralNetworkStyles(),
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1500,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 100,
|
||||
spacingFactor: 1.5,
|
||||
randomize: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 1.2,
|
||||
gravity: 80,
|
||||
idealEdgeLength: 150
|
||||
},
|
||||
wheelSensitivity: 0.1, // Sanfterer Zoom
|
||||
minZoom: 0.3,
|
||||
maxZoom: 2.5,
|
||||
});
|
||||
|
||||
// Daten vom Server laden
|
||||
loadMindmapData(cy);
|
||||
|
||||
// Event-Handler zuweisen
|
||||
setupEventListeners(cy);
|
||||
|
||||
// Globale Referenz für Externe Zugriffe
|
||||
window.cy = cy;
|
||||
window.mindmapInstance = {
|
||||
cy: cy,
|
||||
selectedNode: null,
|
||||
centerNodeInView: function(node) {
|
||||
cy.animate({
|
||||
center: { eles: node },
|
||||
zoom: 1.2,
|
||||
duration: 800,
|
||||
easing: 'ease-in-out-cubic'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 mit zufälligen Werten für neuronales Netzwerk
|
||||
data.nodes.forEach(node => {
|
||||
// Neuronenzell-Größe und Aktivität
|
||||
const neuronSize = Math.floor(Math.random() * 8) + 3;
|
||||
const neuronActivity = Math.random() * 0.7 + 0.3; // Aktivitätslevel zwischen 0.3 und 1.0
|
||||
|
||||
elements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color: node.color_code || '#8B5CF6',
|
||||
isRoot: node.name === 'Wissen',
|
||||
neuronSize: neuronSize,
|
||||
neuronActivity: neuronActivity
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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(),
|
||||
strength: Math.random() * 0.6 + 0.2 // Zufällige Verbindungsstärke zwischen 0.2 und 0.8
|
||||
}
|
||||
});
|
||||
});
|
||||
} 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(),
|
||||
strength: Math.random() * 0.6 + 0.2
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Elemente zu Cytoscape hinzufügen
|
||||
cy.elements().remove();
|
||||
cy.add(elements);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 100,
|
||||
spacingFactor: 1.8,
|
||||
randomize: false,
|
||||
fit: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100
|
||||
}).run();
|
||||
|
||||
// Neuronale Netzwerk-Effekte hinzufügen
|
||||
setTimeout(() => {
|
||||
addNeuralNetworkEffects(cy);
|
||||
}, 2000);
|
||||
|
||||
// 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 mit Neuronen-Eigenschaften
|
||||
fallbackData.nodes.forEach(node => {
|
||||
const neuronSize = Math.floor(Math.random() * 8) + 3;
|
||||
const neuronActivity = Math.random() * 0.7 + 0.3;
|
||||
|
||||
fallbackElements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
color: node.color_code || '#8B5CF6',
|
||||
isRoot: node.name === 'Wissen',
|
||||
neuronSize: neuronSize,
|
||||
neuronActivity: neuronActivity
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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(),
|
||||
strength: Math.random() * 0.6 + 0.2
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Elemente zu Cytoscape hinzufügen
|
||||
cy.elements().remove();
|
||||
cy.add(fallbackElements);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 1800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
fit: true
|
||||
}).run();
|
||||
|
||||
// Neuronale Netzwerk-Effekte hinzufügen
|
||||
setTimeout(() => {
|
||||
addNeuralNetworkEffects(cy);
|
||||
}, 2000);
|
||||
|
||||
// Nach dem Laden Event auslösen
|
||||
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
|
||||
});
|
||||
}
|
||||
|
||||
// Neuronales Netzwerk-Effekte hinzufügen
|
||||
function addNeuralNetworkEffects(cy) {
|
||||
cy.nodes().forEach(node => {
|
||||
const originalPos = node.position();
|
||||
const activity = node.data('neuronActivity') || 0.5;
|
||||
|
||||
// Subtile Pulsierende Bewegung für Neuronen
|
||||
setInterval(() => {
|
||||
const randomFactor = Math.random() * 0.5 + 0.5; // Zufälliger Faktor zwischen 0.5 und 1
|
||||
const offset = (Math.random() - 0.5) * activity;
|
||||
const pulseIntensity = 0.7 + (Math.sin(Date.now() / 2000) * 0.3 * activity * randomFactor);
|
||||
|
||||
// Leichtes Pulsieren und Bewegung basierend auf "Neuronenaktivität"
|
||||
node.animate({
|
||||
position: {
|
||||
x: originalPos.x + offset,
|
||||
y: originalPos.y + offset
|
||||
},
|
||||
style: {
|
||||
'background-opacity': Math.max(0.7, pulseIntensity),
|
||||
'shadow-opacity': Math.max(0.5, pulseIntensity * 0.8)
|
||||
},
|
||||
duration: 3000 + (Math.random() * 2000),
|
||||
easing: 'ease-in-out-cubic',
|
||||
complete: function() {
|
||||
node.animate({
|
||||
position: { x: originalPos.x, y: originalPos.y },
|
||||
style: {
|
||||
'background-opacity': 0.9,
|
||||
'shadow-opacity': 0.6
|
||||
},
|
||||
duration: 3000 + (Math.random() * 2000),
|
||||
easing: 'ease-in-out-cubic'
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 6000 + Math.random() * 4000); // Leicht zufällige Intervalle für organischeres Aussehen
|
||||
|
||||
// Zusätzliche "synaptische" Effekte für Kanten
|
||||
node.connectedEdges().forEach(edge => {
|
||||
const strength = edge.data('strength') || 0.5;
|
||||
|
||||
// Pulsierende Aktivität entlang der Kanten
|
||||
setInterval(() => {
|
||||
const pulseIntensity = 0.5 + (Math.sin(Date.now() / 1500) * 0.5 * strength);
|
||||
|
||||
edge.animate({
|
||||
style: {
|
||||
'line-opacity': Math.max(0.3, pulseIntensity),
|
||||
'width': 1 + (pulseIntensity * 0.6)
|
||||
},
|
||||
duration: 1200,
|
||||
easing: 'ease-in-out'
|
||||
});
|
||||
}, 3000 + (Math.random() * 2000));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Richtet Event-Listener für die Mindmap ein
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function setupEventListeners(cy) {
|
||||
// Klick auf Knoten
|
||||
// Event-Listener für Knoten-Klicks
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
|
||||
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
n.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Speichere ausgewählten Knoten
|
||||
window.mindmapInstance.selectedNode = node;
|
||||
|
||||
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'background-color': node.data('color'),
|
||||
'shadow-color': node.data('color'),
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 15,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
});
|
||||
|
||||
// Verbundene Kanten und Knoten hervorheben
|
||||
const connectedEdges = node.connectedEdges();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
connectedEdges.style({
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'source-arrow-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
});
|
||||
|
||||
connectedNodes.style({
|
||||
'shadow-opacity': 0.7,
|
||||
'shadow-blur': 10,
|
||||
'shadow-color': '#a78bfa'
|
||||
});
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
updateInfoPanel(node);
|
||||
|
||||
// Seitenleiste aktualisieren
|
||||
updateSidebar(node);
|
||||
});
|
||||
|
||||
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth Zoom mit Mouse Wheel
|
||||
cy.on('mousewheel', function(evt) {
|
||||
const delta = evt.originalEvent.deltaY;
|
||||
const factor = delta > 0 ? 0.95 : 1.05;
|
||||
|
||||
cy.animate({
|
||||
zoom: cy.zoom() * factor,
|
||||
duration: 100
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisiert das Info-Panel mit Daten des ausgewählten Knotens
|
||||
function updateInfoPanel(node) {
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
const nodeDescription = document.getElementById('node-description');
|
||||
const connectedNodes = document.getElementById('connected-nodes');
|
||||
const panelTitle = nodeInfoPanel.querySelector('.info-panel-title');
|
||||
|
||||
if (!nodeInfoPanel || !nodeDescription || !connectedNodes) return;
|
||||
|
||||
// Titel und Beschreibung aktualisieren
|
||||
panelTitle.textContent = node.data('name');
|
||||
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
|
||||
|
||||
// Verbundene Knoten anzeigen
|
||||
connectedNodes.innerHTML = '';
|
||||
|
||||
// Verbundene Knoten sammeln (direktes Neighborhood)
|
||||
const connectedNodesList = [];
|
||||
node.neighborhood('node').forEach(n => {
|
||||
if (!connectedNodesList.includes(n) && n.id() !== node.id()) {
|
||||
connectedNodesList.push(n);
|
||||
}
|
||||
});
|
||||
|
||||
// Verbundene Knoten im Panel anzeigen
|
||||
if (connectedNodesList.length > 0) {
|
||||
connectedNodesList.forEach(connectedNode => {
|
||||
const nodeLink = document.createElement('span');
|
||||
nodeLink.className = 'inline-block px-2 py-1 text-xs rounded-md m-1 cursor-pointer';
|
||||
nodeLink.style.backgroundColor = connectedNode.data('color');
|
||||
nodeLink.textContent = connectedNode.data('name');
|
||||
|
||||
// Beim Klick auf den verbundenen Knoten zu diesem wechseln
|
||||
nodeLink.addEventListener('click', function() {
|
||||
resetSelection(cy);
|
||||
|
||||
// Verzögerung vor der neuen Auswahl für besseren visuellen Übergang
|
||||
setTimeout(() => {
|
||||
connectedNode.trigger('tap');
|
||||
}, 50);
|
||||
});
|
||||
|
||||
connectedNodes.appendChild(nodeLink);
|
||||
});
|
||||
} else {
|
||||
connectedNodes.innerHTML = '<span class="text-sm italic">Keine verbundenen Knoten</span>';
|
||||
}
|
||||
|
||||
// Panel anzeigen mit Animation
|
||||
nodeInfoPanel.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
|
||||
nodeInfoPanel.classList.add('visible');
|
||||
}
|
||||
|
||||
// Aktualisiert die Seitenleiste
|
||||
function updateSidebar(node) {
|
||||
// Alle standard Panels ausblenden
|
||||
document.querySelectorAll('[data-sidebar]').forEach(panel => {
|
||||
if (panel.getAttribute('data-sidebar') === 'node-description') {
|
||||
// Beschreibungs-Panel anzeigen
|
||||
panel.classList.remove('hidden');
|
||||
|
||||
// Titel und Beschreibung aktualisieren
|
||||
const titleElement = panel.querySelector('[data-node-title]');
|
||||
const descriptionElement = panel.querySelector('[data-node-description]');
|
||||
|
||||
if (titleElement) {
|
||||
titleElement.textContent = node.data('name');
|
||||
}
|
||||
|
||||
if (descriptionElement) {
|
||||
descriptionElement.innerHTML = formatDescription(node.data('description') || 'Keine Beschreibung verfügbar.');
|
||||
}
|
||||
} else {
|
||||
// Andere Panels ausblenden
|
||||
panel.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Formatiert die Beschreibung mit etwas HTML-Markup
|
||||
function formatDescription(text) {
|
||||
return text
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
// Setzt die Auswahl zurück
|
||||
function resetSelection(cy) {
|
||||
// Alle Stile zurücksetzen
|
||||
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
n.removeStyle();
|
||||
n.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
cy.edges().forEach(e => {
|
||||
e.removeStyle();
|
||||
// Speichere ausgewählten Knoten
|
||||
window.mindmapInstance.selectedNode = node;
|
||||
|
||||
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'background-color': node.data('color'),
|
||||
'shadow-color': node.data('color'),
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 15,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
});
|
||||
|
||||
// Kein Knoten ausgewählt
|
||||
window.mindmapInstance.selectedNode = null;
|
||||
// Verbundene Kanten und Knoten hervorheben
|
||||
const connectedEdges = node.connectedEdges();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
// Info-Panel ausblenden
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
if (nodeInfoPanel) {
|
||||
nodeInfoPanel.classList.remove('visible');
|
||||
connectedEdges.style({
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'source-arrow-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
});
|
||||
|
||||
connectedNodes.style({
|
||||
'shadow-opacity': 0.7,
|
||||
'shadow-blur': 10,
|
||||
'shadow-color': '#a78bfa'
|
||||
});
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
updateInfoPanel(node);
|
||||
|
||||
// Seitenleiste aktualisieren
|
||||
updateSidebar(node);
|
||||
});
|
||||
|
||||
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetSelection(cy);
|
||||
}
|
||||
|
||||
// Standard-Seitenleisten-Panels anzeigen
|
||||
document.querySelectorAll('[data-sidebar]').forEach(panel => {
|
||||
if (panel.getAttribute('data-sidebar') === 'node-description') {
|
||||
panel.classList.add('hidden');
|
||||
} else {
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom-Controls
|
||||
document.getElementById('zoomIn')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('zoomOut')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('resetView')?.addEventListener('click', () => {
|
||||
cy.fit();
|
||||
resetSelection(cy);
|
||||
});
|
||||
|
||||
// Legend-Toggle
|
||||
document.getElementById('toggleLegend')?.addEventListener('click', () => {
|
||||
const legend = document.getElementById('categoryLegend');
|
||||
if (legend) {
|
||||
isLegendVisible = !isLegendVisible;
|
||||
legend.style.display = isLegendVisible ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard-Controls
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === '+' || e.key === '=') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === '-' || e.key === '_') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === 'Escape') {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Aktualisiert das Info-Panel mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateInfoPanel(node) {
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (!infoPanel) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
let html = `
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
infoPanel.innerHTML = html;
|
||||
infoPanel.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Stile für das neuronale Netzwerk-Design zurück
|
||||
* @returns {Array} Stildefinitionen für Cytoscape
|
||||
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function getNeuralNetworkStyles() {
|
||||
return [
|
||||
// Neuronen (Knoten)
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'label': 'data(name)',
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'color': '#ffffff',
|
||||
'text-outline-width': 2,
|
||||
'text-outline-color': '#0a0e19',
|
||||
'text-outline-opacity': 0.9,
|
||||
'font-size': 10,
|
||||
'text-margin-y': 6,
|
||||
'width': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'height': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'background-color': 'data(color)',
|
||||
'background-opacity': 0.9,
|
||||
'border-width': 0,
|
||||
'shape': 'ellipse',
|
||||
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)',
|
||||
'shadow-color': 'data(color)',
|
||||
'shadow-opacity': 0.6,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': 100,
|
||||
'transition-property': 'background-color, shadow-color, shadow-opacity, shadow-blur',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
},
|
||||
// Synapsen (Kanten)
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
'line-color': '#8a8aaa',
|
||||
'width': 'mapData(strength, 0.2, 0.8, 0.8, 2)',
|
||||
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
|
||||
'target-arrow-shape': 'none', // Keine Pfeilspitzen bei Neuronen
|
||||
'target-arrow-color': '#8a8aaa',
|
||||
'arrow-scale': 0.6,
|
||||
'transition-property': 'line-color, line-opacity, width',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
},
|
||||
// Schwache Verbindungen
|
||||
{
|
||||
selector: 'edge[strength <= 0.4]',
|
||||
style: {
|
||||
'line-style': 'dotted'
|
||||
}
|
||||
},
|
||||
// Mittlere Verbindungen
|
||||
{
|
||||
selector: 'edge[strength > 0.4][strength <= 0.6]',
|
||||
style: {
|
||||
'line-style': 'dashed'
|
||||
}
|
||||
},
|
||||
// Starke Verbindungen
|
||||
{
|
||||
selector: 'edge[strength > 0.6]',
|
||||
style: {
|
||||
'line-style': 'solid'
|
||||
}
|
||||
},
|
||||
// Wurzelknoten (speziell gestaltet)
|
||||
{
|
||||
selector: 'node[isRoot]',
|
||||
style: {
|
||||
'font-size': 12,
|
||||
'font-weight': 'bold',
|
||||
'width': 50,
|
||||
'height': 50,
|
||||
'background-color': '#6366f1',
|
||||
'shadow-blur': 20,
|
||||
'shadow-color': '#6366f1',
|
||||
'shadow-opacity': 0.8,
|
||||
'text-margin-y': 8
|
||||
}
|
||||
},
|
||||
// Hover-Effekt für Knoten
|
||||
{
|
||||
selector: 'node:hover',
|
||||
style: {
|
||||
'shadow-blur': 20,
|
||||
'shadow-opacity': 0.9,
|
||||
'transition-property': 'shadow-opacity, shadow-blur',
|
||||
'transition-duration': '0.2s'
|
||||
}
|
||||
},
|
||||
// Hover-Effekt für Kanten
|
||||
{
|
||||
selector: 'edge:hover',
|
||||
style: {
|
||||
'line-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
function updateSidebar(node) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
let html = `
|
||||
<div class="node-details">
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Auswahl zurück
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function resetSelection(cy) {
|
||||
window.mindmapInstance.selectedNode = null;
|
||||
|
||||
// Alle Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(node => {
|
||||
node.removeStyle();
|
||||
node.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Info-Panel ausblenden
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (infoPanel) {
|
||||
infoPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
// Seitenleiste leeren
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.innerHTML = '';
|
||||
}
|
||||
}
|
||||
@@ -4,238 +4,216 @@
|
||||
*/
|
||||
|
||||
// Globale Variablen
|
||||
let cy;
|
||||
let selectedNode = null;
|
||||
let isLegendVisible = true;
|
||||
|
||||
// Initialisierung der Mindmap
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Cytoscape-Container initialisieren
|
||||
cy = cytoscape({
|
||||
container: document.getElementById('cy'),
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#60a5fa',
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '100px',
|
||||
'font-size': '12px',
|
||||
'color': '#fff',
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-width': '2px',
|
||||
'width': '40px',
|
||||
'height': '40px',
|
||||
'border-width': '2px',
|
||||
'border-color': '#fff',
|
||||
'border-opacity': '0.5',
|
||||
'padding': '10px',
|
||||
'text-events': 'yes'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': '2px',
|
||||
'line-color': 'rgba(255, 255, 255, 0.3)',
|
||||
'target-arrow-color': 'rgba(255, 255, 255, 0.3)',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
'label': 'data(label)',
|
||||
'font-size': '10px',
|
||||
'color': '#fff',
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-width': '2px',
|
||||
'text-rotation': 'autorotate'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: ':selected',
|
||||
style: {
|
||||
'background-color': '#8b5cf6',
|
||||
'line-color': '#8b5cf6',
|
||||
'target-arrow-color': '#8b5cf6',
|
||||
'source-arrow-color': '#8b5cf6',
|
||||
'text-outline-color': '#000',
|
||||
'text-outline-width': '2px',
|
||||
'border-width': '3px',
|
||||
'border-color': '#fff',
|
||||
'border-opacity': '1'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: '.highlighted',
|
||||
style: {
|
||||
'background-color': '#10b981',
|
||||
'line-color': '#10b981',
|
||||
'target-arrow-color': '#10b981',
|
||||
'source-arrow-color': '#10b981',
|
||||
'transition-property': 'background-color, line-color, target-arrow-color',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
idealEdgeLength: 100,
|
||||
nodeOverlap: 20,
|
||||
refresh: 20,
|
||||
fit: true,
|
||||
padding: 30,
|
||||
randomize: false,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 400000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 5,
|
||||
gravity: 80,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0
|
||||
}
|
||||
});
|
||||
document.addEventListener('mindmap-loaded', function() {
|
||||
const cy = window.cy;
|
||||
if (!cy) return;
|
||||
|
||||
// Event-Listener für Knoten
|
||||
// Event-Listener für Knoten-Klicks
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
updateNodeInfo(node);
|
||||
highlightConnectedNodes(node);
|
||||
|
||||
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
n.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Speichere ausgewählten Knoten
|
||||
selectedNode = node;
|
||||
|
||||
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'background-color': node.data('color'),
|
||||
'shadow-color': node.data('color'),
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 15,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
});
|
||||
|
||||
// Verbundene Kanten und Knoten hervorheben
|
||||
const connectedEdges = node.connectedEdges();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
connectedEdges.style({
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'source-arrow-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
});
|
||||
|
||||
connectedNodes.style({
|
||||
'shadow-opacity': 0.7,
|
||||
'shadow-blur': 10,
|
||||
'shadow-color': '#a78bfa'
|
||||
});
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
updateInfoPanel(node);
|
||||
|
||||
// Seitenleiste aktualisieren
|
||||
updateSidebar(node);
|
||||
});
|
||||
|
||||
// Event-Listener für Hintergrund-Klick
|
||||
|
||||
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetHighlighting();
|
||||
hideNodeInfo();
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom-Kontrollen
|
||||
document.getElementById('zoom-in').addEventListener('click', function() {
|
||||
// Zoom-Controls
|
||||
document.getElementById('zoomIn')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('zoom-out').addEventListener('click', function() {
|
||||
document.getElementById('zoomOut')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('reset-view').addEventListener('click', function() {
|
||||
document.getElementById('resetView')?.addEventListener('click', () => {
|
||||
cy.fit();
|
||||
resetSelection(cy);
|
||||
});
|
||||
|
||||
// Legende ein-/ausblenden
|
||||
document.getElementById('toggle-legend').addEventListener('click', function() {
|
||||
const legend = document.querySelector('.category-legend');
|
||||
isLegendVisible = !isLegendVisible;
|
||||
legend.style.display = isLegendVisible ? 'flex' : 'none';
|
||||
// Legend-Toggle
|
||||
document.getElementById('toggleLegend')?.addEventListener('click', () => {
|
||||
const legend = document.getElementById('categoryLegend');
|
||||
if (legend) {
|
||||
isLegendVisible = !isLegendVisible;
|
||||
legend.style.display = isLegendVisible ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Tastatursteuerung
|
||||
document.addEventListener('keydown', function(evt) {
|
||||
switch(evt.key) {
|
||||
case '+':
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
break;
|
||||
case '-':
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
break;
|
||||
case 'Escape':
|
||||
resetHighlighting();
|
||||
hideNodeInfo();
|
||||
break;
|
||||
// Keyboard-Controls
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === '+' || e.key === '=') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === '-' || e.key === '_') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === 'Escape') {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Knoteninformationen aktualisieren
|
||||
function updateNodeInfo(node) {
|
||||
const infoPanel = document.getElementById('node-info');
|
||||
const infoContent = infoPanel.querySelector('.info-content');
|
||||
/**
|
||||
* Aktualisiert das Info-Panel mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateInfoPanel(node) {
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (!infoPanel) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
// Knotendaten abrufen
|
||||
const nodeData = node.data();
|
||||
let html = `
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
infoContent.innerHTML = `
|
||||
<h4 class="text-lg font-semibold mb-2">${nodeData.label}</h4>
|
||||
<p class="mb-3">${nodeData.description || 'Keine Beschreibung verfügbar.'}</p>
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-semibold mb-2">Verknüpfte Konzepte:</h5>
|
||||
<ul class="space-y-1">
|
||||
${getConnectedNodesList(node)}
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Panel anzeigen
|
||||
infoPanel.classList.add('visible');
|
||||
infoPanel.innerHTML = html;
|
||||
infoPanel.style.display = 'block';
|
||||
}
|
||||
|
||||
// Verbundene Knoten hervorheben
|
||||
function highlightConnectedNodes(node) {
|
||||
// Vorherige Hervorhebungen zurücksetzen
|
||||
resetHighlighting();
|
||||
|
||||
// Ausgewählten Knoten hervorheben
|
||||
node.addClass('highlighted');
|
||||
|
||||
// Verbundene Knoten und Kanten hervorheben
|
||||
const connectedElements = node.neighborhood();
|
||||
connectedElements.addClass('highlighted');
|
||||
}
|
||||
/**
|
||||
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateSidebar(node) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
// Hervorhebungen zurücksetzen
|
||||
function resetHighlighting() {
|
||||
cy.elements().removeClass('highlighted');
|
||||
}
|
||||
|
||||
// Info-Panel ausblenden
|
||||
function hideNodeInfo() {
|
||||
const infoPanel = document.getElementById('node-info');
|
||||
infoPanel.classList.remove('visible');
|
||||
}
|
||||
|
||||
// Liste der verbundenen Knoten generieren
|
||||
function getConnectedNodesList(node) {
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
if (connectedNodes.length === 0) {
|
||||
return '<li class="text-gray-400">Keine direkten Verbindungen</li>';
|
||||
}
|
||||
|
||||
return connectedNodes.map(connectedNode => {
|
||||
const data = connectedNode.data();
|
||||
return `
|
||||
<li class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full" style="background-color: ${getNodeColor(data.category)}"></span>
|
||||
<span>${data.label}</span>
|
||||
let html = `
|
||||
<div class="node-details">
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
// Farbe basierend auf Kategorie
|
||||
function getNodeColor(category) {
|
||||
const colors = {
|
||||
'Philosophie': '#60a5fa',
|
||||
'Wissenschaft': '#8b5cf6',
|
||||
'Technologie': '#10b981',
|
||||
'Künste': '#f59e0b',
|
||||
'Psychologie': '#ef4444'
|
||||
};
|
||||
return colors[category] || '#60a5fa';
|
||||
/**
|
||||
* Setzt die Auswahl zurück
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function resetSelection(cy) {
|
||||
selectedNode = null;
|
||||
|
||||
// Alle Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(node => {
|
||||
node.removeStyle();
|
||||
node.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Info-Panel ausblenden
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (infoPanel) {
|
||||
infoPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
// Seitenleiste leeren
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.innerHTML = '';
|
||||
}
|
||||
}
|
||||
1353
static/js/mindmap.js
1353
static/js/mindmap.js
File diff suppressed because it is too large
Load Diff
@@ -26,413 +26,526 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function initMindmapPage() {
|
||||
console.log('Mindmap-Seite wird initialisiert...');
|
||||
|
||||
// Hauptcontainer für die Mindmap
|
||||
const cyContainer = document.getElementById('cy');
|
||||
if (!cyContainer) {
|
||||
console.error('Mindmap-Container #cy nicht gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Info-Panel für Knotendetails
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
const nodeDescription = document.getElementById('node-description');
|
||||
const connectedNodes = document.getElementById('connected-nodes');
|
||||
|
||||
// Toolbar-Buttons
|
||||
const fitButton = document.getElementById('fit-btn');
|
||||
const resetButton = document.getElementById('reset-btn');
|
||||
const toggleLabelsButton = document.getElementById('toggle-labels-btn');
|
||||
|
||||
// Mindmap-Instanz
|
||||
let mindmap = null;
|
||||
|
||||
// Cytoscape.js für die Visualisierung initialisieren
|
||||
try {
|
||||
// Cytoscape.js-Bibliothek überprüfen
|
||||
if (typeof cytoscape === 'undefined') {
|
||||
loadExternalScript('https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js')
|
||||
.then(() => {
|
||||
initCytoscape();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden von Cytoscape.js:', error);
|
||||
showErrorMessage(cyContainer, 'Cytoscape.js konnte nicht geladen werden.');
|
||||
});
|
||||
} else {
|
||||
initCytoscape();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Initialisierung der Mindmap:', error);
|
||||
showErrorMessage(cyContainer, 'Die Mindmap konnte nicht initialisiert werden: ' + error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt ein externes Script asynchron
|
||||
*/
|
||||
function loadExternalScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Fehlermeldung im Container an
|
||||
*/
|
||||
function showErrorMessage(container, message) {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert Cytoscape mit den Mindmap-Daten
|
||||
*/
|
||||
function initCytoscape() {
|
||||
console.log('Cytoscape.js wird initialisiert...');
|
||||
|
||||
// Zeige Ladeanimation
|
||||
cyContainer.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>
|
||||
`;
|
||||
|
||||
// Lade Daten vom Backend
|
||||
fetch('/api/mindmap')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkfehler beim Laden der Mindmap-Daten');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Mindmap-Daten erfolgreich geladen');
|
||||
renderMindmap(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||
|
||||
// Verwende Standarddaten als Fallback
|
||||
console.log('Verwende Standarddaten als Fallback...');
|
||||
const defaultData = generateDefaultData();
|
||||
renderMindmap(defaultData);
|
||||
// Warte auf die Cytoscape-Instanz
|
||||
document.addEventListener('mindmap-loaded', function() {
|
||||
const cy = window.cy;
|
||||
if (!cy) return;
|
||||
|
||||
// Event-Listener für Knoten-Klicks
|
||||
cy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
|
||||
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(n => {
|
||||
n.removeStyle();
|
||||
n.connectedEdges().removeStyle();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Standarddaten für die Mindmap als Fallback
|
||||
*/
|
||||
function 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 Mindmap mit Cytoscape.js
|
||||
*/
|
||||
function renderMindmap(data) {
|
||||
console.log('Rendere Mindmap mit Daten:', data);
|
||||
|
||||
// Speichere ausgewählten Knoten
|
||||
window.mindmapInstance.selectedNode = node;
|
||||
|
||||
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'background-color': node.data('color'),
|
||||
'shadow-color': node.data('color'),
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 15,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0
|
||||
});
|
||||
|
||||
// Verbundene Kanten und Knoten hervorheben
|
||||
const connectedEdges = node.connectedEdges();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
connectedEdges.style({
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'source-arrow-color': '#a78bfa',
|
||||
'line-opacity': 0.8,
|
||||
'width': 2
|
||||
});
|
||||
|
||||
connectedNodes.style({
|
||||
'shadow-opacity': 0.7,
|
||||
'shadow-blur': 10,
|
||||
'shadow-color': '#a78bfa'
|
||||
});
|
||||
|
||||
// Info-Panel aktualisieren
|
||||
updateInfoPanel(node);
|
||||
|
||||
// Seitenleiste aktualisieren
|
||||
updateSidebar(node);
|
||||
});
|
||||
|
||||
// Konvertiere Backend-Daten in Cytoscape-Format
|
||||
const elements = convertToCytoscapeFormat(data);
|
||||
|
||||
// Leere den Container
|
||||
cyContainer.innerHTML = '';
|
||||
|
||||
// Erstelle Cytoscape-Instanz
|
||||
mindmap = cytoscape({
|
||||
container: cyContainer,
|
||||
elements: elements,
|
||||
style: [
|
||||
{
|
||||
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': document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#334155',
|
||||
'text-background-color': document.documentElement.classList.contains('dark') ? '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': document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
||||
'target-arrow-color': document.documentElement.classList.contains('dark') ? '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
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
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
|
||||
// Klick auf Hintergrund - Auswahl zurücksetzen
|
||||
cy.on('tap', function(evt) {
|
||||
if (evt.target === cy) {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
|
||||
// Event-Listener für Knoteninteraktionen
|
||||
mindmap.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
const nodeData = node.data();
|
||||
|
||||
// Update Info-Panel
|
||||
updateNodeInfoPanel(nodeData);
|
||||
|
||||
// Lade verbundene Knoten
|
||||
updateConnectedNodes(node);
|
||||
|
||||
// Zoom-Controls
|
||||
document.getElementById('zoomIn')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
});
|
||||
|
||||
// Toolbar-Buttons aktivieren
|
||||
if (fitButton) {
|
||||
fitButton.addEventListener('click', () => {
|
||||
mindmap.fit();
|
||||
mindmap.center();
|
||||
|
||||
document.getElementById('zoomOut')?.addEventListener('click', () => {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
}
|
||||
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', () => {
|
||||
mindmap.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
randomize: true,
|
||||
fit: true
|
||||
}).run();
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleLabelsButton) {
|
||||
let labelsVisible = true;
|
||||
toggleLabelsButton.addEventListener('click', () => {
|
||||
labelsVisible = !labelsVisible;
|
||||
|
||||
if (labelsVisible) {
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style('label', 'data(name)')
|
||||
.update();
|
||||
} else {
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style('label', '')
|
||||
.update();
|
||||
});
|
||||
|
||||
document.getElementById('resetView')?.addEventListener('click', () => {
|
||||
cy.fit();
|
||||
resetSelection(cy);
|
||||
});
|
||||
|
||||
// Legend-Toggle
|
||||
document.getElementById('toggleLegend')?.addEventListener('click', () => {
|
||||
const legend = document.getElementById('categoryLegend');
|
||||
if (legend) {
|
||||
isLegendVisible = !isLegendVisible;
|
||||
legend.style.display = isLegendVisible ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard-Controls
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === '+' || e.key === '=') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() * 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === '-' || e.key === '_') {
|
||||
cy.zoom({
|
||||
level: cy.zoom() / 1.2,
|
||||
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
|
||||
});
|
||||
} else if (e.key === 'Escape') {
|
||||
resetSelection(cy);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Info-Panel mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateInfoPanel(node) {
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (!infoPanel) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
let html = `
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
infoPanel.innerHTML = html;
|
||||
infoPanel.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||
* @param {Object} node - Der ausgewählte Knoten
|
||||
*/
|
||||
function updateSidebar(node) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
const data = node.data();
|
||||
const connectedNodes = node.neighborhood('node');
|
||||
|
||||
let html = `
|
||||
<div class="node-details">
|
||||
<h3>${data.label || data.name}</h3>
|
||||
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||
<div class="connections">
|
||||
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
connectedNodes.forEach(connectedNode => {
|
||||
const connectedData = connectedNode.data();
|
||||
html += `
|
||||
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||
${connectedData.label || connectedData.name}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Auswahl zurück
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function resetSelection(cy) {
|
||||
window.mindmapInstance.selectedNode = null;
|
||||
|
||||
// Alle Hervorhebungen zurücksetzen
|
||||
cy.nodes().forEach(node => {
|
||||
node.removeStyle();
|
||||
node.connectedEdges().removeStyle();
|
||||
});
|
||||
|
||||
// Info-Panel ausblenden
|
||||
const infoPanel = document.getElementById('infoPanel');
|
||||
if (infoPanel) {
|
||||
infoPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
// Seitenleiste leeren
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Standarddaten für die Mindmap als Fallback
|
||||
*/
|
||||
function 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 Mindmap mit Cytoscape.js
|
||||
*/
|
||||
function renderMindmap(data) {
|
||||
console.log('Rendere Mindmap mit Daten:', data);
|
||||
|
||||
// Konvertiere Backend-Daten in Cytoscape-Format
|
||||
const elements = convertToCytoscapeFormat(data);
|
||||
|
||||
// Leere den Container
|
||||
cyContainer.innerHTML = '';
|
||||
|
||||
// Erstelle Cytoscape-Instanz
|
||||
mindmap = cytoscape({
|
||||
container: cyContainer,
|
||||
elements: elements,
|
||||
style: [
|
||||
{
|
||||
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': document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#334155',
|
||||
'text-background-color': document.documentElement.classList.contains('dark') ? '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': document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
||||
'target-arrow-color': document.documentElement.classList.contains('dark') ? '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
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
// Event-Listener für Knoteninteraktionen
|
||||
mindmap.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
const nodeData = node.data();
|
||||
|
||||
// Dark Mode-Änderungen überwachen
|
||||
document.addEventListener('darkModeToggled', function(event) {
|
||||
updateDarkModeStyles(event.detail.isDark);
|
||||
});
|
||||
// Update Info-Panel
|
||||
updateNodeInfoPanel(nodeData);
|
||||
|
||||
// Initial fit und center
|
||||
setTimeout(() => {
|
||||
// Lade verbundene Knoten
|
||||
updateConnectedNodes(node);
|
||||
});
|
||||
|
||||
// Toolbar-Buttons aktivieren
|
||||
if (fitButton) {
|
||||
fitButton.addEventListener('click', () => {
|
||||
mindmap.fit();
|
||||
mindmap.center();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert die Backend-Daten ins Cytoscape-Format
|
||||
*/
|
||||
function convertToCytoscapeFormat(data) {
|
||||
const elements = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
|
||||
// Nodes hinzufügen
|
||||
if (data.nodes && data.nodes.length > 0) {
|
||||
data.nodes.forEach(node => {
|
||||
elements.nodes.push({
|
||||
data: {
|
||||
id: String(node.id),
|
||||
name: node.name,
|
||||
description: node.description || 'Keine Beschreibung verfügbar',
|
||||
category: node.category || 'Allgemein',
|
||||
color: node.color_code || getRandomColor()
|
||||
}
|
||||
});
|
||||
|
||||
// Kante zum Elternknoten hinzufügen (falls vorhanden)
|
||||
if (node.parent_id) {
|
||||
elements.edges.push({
|
||||
data: {
|
||||
id: `edge-${node.parent_id}-${node.id}`,
|
||||
source: String(node.parent_id),
|
||||
target: String(node.id)
|
||||
}
|
||||
});
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', () => {
|
||||
mindmap.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
randomize: true,
|
||||
fit: true
|
||||
}).run();
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleLabelsButton) {
|
||||
let labelsVisible = true;
|
||||
toggleLabelsButton.addEventListener('click', () => {
|
||||
labelsVisible = !labelsVisible;
|
||||
|
||||
if (labelsVisible) {
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style('label', 'data(name)')
|
||||
.update();
|
||||
} else {
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style('label', '')
|
||||
.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dark Mode-Änderungen überwachen
|
||||
document.addEventListener('darkModeToggled', function(event) {
|
||||
updateDarkModeStyles(event.detail.isDark);
|
||||
});
|
||||
|
||||
// Initial fit und center
|
||||
setTimeout(() => {
|
||||
mindmap.fit();
|
||||
mindmap.center();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert die Backend-Daten ins Cytoscape-Format
|
||||
*/
|
||||
function convertToCytoscapeFormat(data) {
|
||||
const elements = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
|
||||
// Nodes hinzufügen
|
||||
if (data.nodes && data.nodes.length > 0) {
|
||||
data.nodes.forEach(node => {
|
||||
elements.nodes.push({
|
||||
data: {
|
||||
id: String(node.id),
|
||||
name: node.name,
|
||||
description: node.description || 'Keine Beschreibung verfügbar',
|
||||
category: node.category || 'Allgemein',
|
||||
color: node.color_code || getRandomColor()
|
||||
}
|
||||
});
|
||||
|
||||
// Zusätzliche Kanten zwischen Knoten hinzufügen (falls in den Daten vorhanden)
|
||||
if (data.edges && data.edges.length > 0) {
|
||||
data.edges.forEach(edge => {
|
||||
elements.edges.push({
|
||||
data: {
|
||||
id: `edge-${edge.source}-${edge.target}`,
|
||||
source: String(edge.source),
|
||||
target: String(edge.target)
|
||||
}
|
||||
});
|
||||
// Kante zum Elternknoten hinzufügen (falls vorhanden)
|
||||
if (node.parent_id) {
|
||||
elements.edges.push({
|
||||
data: {
|
||||
id: `edge-${node.parent_id}-${node.id}`,
|
||||
source: String(node.parent_id),
|
||||
target: String(node.id)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Informations-Panel mit den Knotendaten
|
||||
*/
|
||||
function updateNodeInfoPanel(nodeData) {
|
||||
if (nodeInfoPanel && nodeDescription) {
|
||||
// Panel anzeigen
|
||||
nodeInfoPanel.style.display = 'block';
|
||||
|
||||
// Titel und Beschreibung aktualisieren
|
||||
const titleElement = nodeInfoPanel.querySelector('.info-panel-title');
|
||||
if (titleElement) {
|
||||
titleElement.textContent = nodeData.name;
|
||||
}
|
||||
|
||||
if (nodeDescription) {
|
||||
nodeDescription.textContent = nodeData.description || 'Keine Beschreibung verfügbar';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Liste der verbundenen Knoten
|
||||
*/
|
||||
function updateConnectedNodes(node) {
|
||||
if (connectedNodes) {
|
||||
// Leere den Container
|
||||
connectedNodes.innerHTML = '';
|
||||
|
||||
// Hole verbundene Knoten
|
||||
const connectedEdges = node.connectedEdges();
|
||||
|
||||
if (connectedEdges.length === 0) {
|
||||
connectedNodes.innerHTML = '<div class="text-sm italic">Keine verbundenen Knoten</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Füge alle verbundenen Knoten hinzu
|
||||
connectedEdges.forEach(edge => {
|
||||
const targetNode = edge.target().id() === node.id() ? edge.source() : edge.target();
|
||||
const targetData = targetNode.data();
|
||||
|
||||
const nodeLink = document.createElement('div');
|
||||
nodeLink.className = 'node-link';
|
||||
nodeLink.innerHTML = `
|
||||
<span class="w-2 h-2 rounded-full" style="background-color: ${targetData.color}"></span>
|
||||
${targetData.name}
|
||||
`;
|
||||
|
||||
// Klick-Event zum Fokussieren des Knotens
|
||||
nodeLink.addEventListener('click', () => {
|
||||
mindmap.center(targetNode);
|
||||
targetNode.select();
|
||||
updateNodeInfoPanel(targetData);
|
||||
updateConnectedNodes(targetNode);
|
||||
// Zusätzliche Kanten zwischen Knoten hinzufügen (falls in den Daten vorhanden)
|
||||
if (data.edges && data.edges.length > 0) {
|
||||
data.edges.forEach(edge => {
|
||||
elements.edges.push({
|
||||
data: {
|
||||
id: `edge-${edge.source}-${edge.target}`,
|
||||
source: String(edge.source),
|
||||
target: String(edge.target)
|
||||
}
|
||||
});
|
||||
|
||||
connectedNodes.appendChild(nodeLink);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Styles bei Dark Mode-Änderungen
|
||||
*/
|
||||
function updateDarkModeStyles(isDark) {
|
||||
if (!mindmap) return;
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert das Informations-Panel mit den Knotendaten
|
||||
*/
|
||||
function updateNodeInfoPanel(nodeData) {
|
||||
if (nodeInfoPanel && nodeDescription) {
|
||||
// Panel anzeigen
|
||||
nodeInfoPanel.style.display = 'block';
|
||||
|
||||
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)';
|
||||
// Titel und Beschreibung aktualisieren
|
||||
const titleElement = nodeInfoPanel.querySelector('.info-panel-title');
|
||||
if (titleElement) {
|
||||
titleElement.textContent = nodeData.name;
|
||||
}
|
||||
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style({
|
||||
'color': textColor,
|
||||
'text-background-color': textBgColor
|
||||
})
|
||||
.selector('edge')
|
||||
.style({
|
||||
'line-color': edgeColor,
|
||||
'target-arrow-color': edgeColor
|
||||
})
|
||||
.update();
|
||||
if (nodeDescription) {
|
||||
nodeDescription.textContent = nodeData.description || 'Keine Beschreibung verfügbar';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Liste der verbundenen Knoten
|
||||
*/
|
||||
function updateConnectedNodes(node) {
|
||||
if (connectedNodes) {
|
||||
// Leere den Container
|
||||
connectedNodes.innerHTML = '';
|
||||
|
||||
// Hole verbundene Knoten
|
||||
const connectedEdges = node.connectedEdges();
|
||||
|
||||
if (connectedEdges.length === 0) {
|
||||
connectedNodes.innerHTML = '<div class="text-sm italic">Keine verbundenen Knoten</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Füge alle verbundenen Knoten hinzu
|
||||
connectedEdges.forEach(edge => {
|
||||
const targetNode = edge.target().id() === node.id() ? edge.source() : edge.target();
|
||||
const targetData = targetNode.data();
|
||||
|
||||
const nodeLink = document.createElement('div');
|
||||
nodeLink.className = 'node-link';
|
||||
nodeLink.innerHTML = `
|
||||
<span class="w-2 h-2 rounded-full" style="background-color: ${targetData.color}"></span>
|
||||
${targetData.name}
|
||||
`;
|
||||
|
||||
// Klick-Event zum Fokussieren des Knotens
|
||||
nodeLink.addEventListener('click', () => {
|
||||
mindmap.center(targetNode);
|
||||
targetNode.select();
|
||||
updateNodeInfoPanel(targetData);
|
||||
updateConnectedNodes(targetNode);
|
||||
});
|
||||
|
||||
connectedNodes.appendChild(nodeLink);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Styles bei Dark Mode-Änderungen
|
||||
*/
|
||||
function updateDarkModeStyles(isDark) {
|
||||
if (!mindmap) return;
|
||||
|
||||
/**
|
||||
* Generiert eine zufällige Farbe
|
||||
*/
|
||||
function getRandomColor() {
|
||||
const colors = [
|
||||
'#4299E1', // Blau
|
||||
'#9F7AEA', // Lila
|
||||
'#48BB78', // Grün
|
||||
'#ED8936', // Orange
|
||||
'#ED64A6', // Pink
|
||||
'#F56565' // Rot
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
}
|
||||
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)';
|
||||
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style({
|
||||
'color': textColor,
|
||||
'text-background-color': textBgColor
|
||||
})
|
||||
.selector('edge')
|
||||
.style({
|
||||
'line-color': edgeColor,
|
||||
'target-arrow-color': edgeColor
|
||||
})
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine zufällige Farbe
|
||||
*/
|
||||
function getRandomColor() {
|
||||
const colors = [
|
||||
'#4299E1', // Blau
|
||||
'#9F7AEA', // Lila
|
||||
'#48BB78', // Grün
|
||||
'#ED8936', // Orange
|
||||
'#ED64A6', // Pink
|
||||
'#F56565' // Rot
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
|
||||
// Initialisiere die Mindmap-Seite
|
||||
initMindmapPage();
|
||||
@@ -6,19 +6,83 @@
|
||||
|
||||
// Warte bis DOM geladen ist
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||
console.log('DOMContentLoaded Event ausgelöst');
|
||||
|
||||
// Prüfe, ob der Container existiert
|
||||
const cyContainer = document.getElementById('cy');
|
||||
console.log('Container gefunden:', cyContainer);
|
||||
|
||||
if (!cyContainer) {
|
||||
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
|
||||
console.error('Mindmap-Container #cy nicht gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe, ob Cytoscape verfügbar ist
|
||||
if (typeof cytoscape === 'undefined') {
|
||||
console.error('Cytoscape ist nicht definiert!');
|
||||
return;
|
||||
}
|
||||
console.log('Cytoscape ist verfügbar');
|
||||
|
||||
// Beispiel-Daten (kannst du später ersetzen)
|
||||
const elements = [
|
||||
{ data: { id: 'a', label: 'Neuron A' } },
|
||||
{ data: { id: 'b', label: 'Neuron B' } },
|
||||
{ data: { id: 'c', label: 'Neuron C' } },
|
||||
{ data: { source: 'a', target: 'b' } },
|
||||
{ data: { source: 'a', target: 'c' } }
|
||||
];
|
||||
|
||||
console.log('Initialisiere Cytoscape...');
|
||||
|
||||
// Auf das Laden der Mindmap warten
|
||||
document.addEventListener('mindmap-loaded', function() {
|
||||
console.log('Mindmap geladen, wende neuronales Netzwerk-Design an...');
|
||||
updateMindmap();
|
||||
// Initialisiere Cytoscape
|
||||
window.cy = cytoscape({
|
||||
container: cyContainer,
|
||||
elements: elements,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#60a5fa',
|
||||
'label': 'data(label)',
|
||||
'color': '#fff',
|
||||
'text-background-color': '#222a',
|
||||
'text-background-opacity': 0.7,
|
||||
'text-background-padding': '4px',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': 18,
|
||||
'width': 40,
|
||||
'height': 40,
|
||||
'border-width': 4,
|
||||
'border-color': '#a78bfa',
|
||||
'shadow-blur': 20,
|
||||
'shadow-color': '#a78bfa',
|
||||
'shadow-opacity': 0.7,
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 4,
|
||||
'line-color': '#a78bfa',
|
||||
'target-arrow-color': '#a78bfa',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Cytoscape initialisiert');
|
||||
|
||||
// Event auslösen, damit andere Scripte reagieren können
|
||||
document.dispatchEvent(new Event('mindmap-loaded'));
|
||||
console.log('mindmap-loaded Event ausgelöst');
|
||||
});
|
||||
|
||||
// Mindmap-Daten
|
||||
@@ -158,6 +222,15 @@ const mindmapData = {
|
||||
]
|
||||
};
|
||||
|
||||
// Kategorie-Farben definieren
|
||||
const categoryColors = {
|
||||
'Philosophie': '#60a5fa',
|
||||
'Wissenschaft': '#8b5cf6',
|
||||
'Technologie': '#10b981',
|
||||
'Künste': '#f59e0b',
|
||||
'Psychologie': '#ef4444'
|
||||
};
|
||||
|
||||
// Mindmap aktualisieren
|
||||
function updateMindmap() {
|
||||
if (!cy) return;
|
||||
@@ -173,7 +246,13 @@ function updateMindmap() {
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
category: node.category,
|
||||
description: node.description
|
||||
description: node.description,
|
||||
color: categoryColors[node.category] || '#60a5fa',
|
||||
neuronSize: Math.floor(Math.random() * 8) + 3,
|
||||
neuronActivity: Math.random() * 0.7 + 0.3,
|
||||
pulseFrequency: Math.random() * 4 + 2,
|
||||
refractionPeriod: Math.random() * 300 + 700,
|
||||
threshold: Math.random() * 0.3 + 0.6
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -185,30 +264,126 @@ function updateMindmap() {
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.label
|
||||
label: edge.label,
|
||||
strength: Math.random() * 0.6 + 0.2,
|
||||
conductionVelocity: Math.random() * 0.5 + 0.3,
|
||||
latency: Math.random() * 100 + 50
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Neuronales Netzwerk-Styling anwenden
|
||||
cy.style([
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '100px',
|
||||
'font-size': '14px',
|
||||
'color': '#ffffff',
|
||||
'text-outline-color': '#0a0e19',
|
||||
'text-outline-width': 1.5,
|
||||
'text-outline-opacity': 0.9,
|
||||
'text-margin-y': 7,
|
||||
'width': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'height': 'mapData(neuronSize, 3, 10, 15, 40)',
|
||||
'background-color': 'data(color)',
|
||||
'background-opacity': 0.85,
|
||||
'border-width': 0,
|
||||
'shape': 'ellipse',
|
||||
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)',
|
||||
'shadow-color': 'data(color)',
|
||||
'shadow-opacity': 0.6,
|
||||
'shadow-offset-x': 0,
|
||||
'shadow-offset-y': 0,
|
||||
'text-background-color': 'rgba(24, 28, 45, 0.7)',
|
||||
'text-background-opacity': 0.8,
|
||||
'text-background-shape': 'roundrectangle',
|
||||
'text-background-padding': '4px',
|
||||
'transition-property': 'background-color, shadow-blur, shadow-opacity, background-opacity',
|
||||
'transition-duration': '0.5s'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 'mapData(strength, 0.2, 0.8, 0.7, 2)',
|
||||
'curve-style': 'bezier',
|
||||
'line-color': '#8a8aaa',
|
||||
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
|
||||
'line-style': function(ele) {
|
||||
const strength = ele.data('strength');
|
||||
if (strength <= 0.4) return 'dotted';
|
||||
if (strength <= 0.6) return 'dashed';
|
||||
return 'solid';
|
||||
},
|
||||
'target-arrow-shape': 'none',
|
||||
'source-endpoint': '0% 50%',
|
||||
'target-endpoint': '100% 50%',
|
||||
'transition-property': 'line-color, width, line-style, line-opacity',
|
||||
'transition-duration': '0.4s'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: ':selected',
|
||||
style: {
|
||||
'background-color': '#f59e0b',
|
||||
'shadow-blur': 32,
|
||||
'shadow-color': '#f59e0b',
|
||||
'shadow-opacity': 1,
|
||||
'border-width': 4,
|
||||
'border-color': '#fff'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: '.highlighted',
|
||||
style: {
|
||||
'background-color': '#10b981',
|
||||
'shadow-blur': 28,
|
||||
'shadow-color': '#10b981',
|
||||
'shadow-opacity': 1,
|
||||
'line-color': '#10b981',
|
||||
'target-arrow-color': '#10b981',
|
||||
'transition-property': 'background-color, shadow-blur, shadow-opacity, line-color',
|
||||
'transition-duration': '0.3s'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// Layout anwenden
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
idealEdgeLength: 100,
|
||||
nodeOverlap: 20,
|
||||
refresh: 20,
|
||||
fit: true,
|
||||
padding: 30,
|
||||
animate: true,
|
||||
animationDuration: 1800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 100,
|
||||
spacingFactor: 1.8,
|
||||
randomize: false,
|
||||
fit: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 400000,
|
||||
nodeRepulsion: 8000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 5,
|
||||
gravity: 80,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0
|
||||
nestingFactor: 1.2,
|
||||
gravity: 80
|
||||
}).run();
|
||||
|
||||
// Pulsierende Soma-Effekte starten
|
||||
if (window.pulseInterval) clearInterval(window.pulseInterval);
|
||||
window.pulseInterval = setInterval(() => {
|
||||
cy.nodes().forEach(node => {
|
||||
const baseBlur = 18;
|
||||
const pulse = 0.7 + 0.3 * Math.sin(Date.now() / 400 + node.id().charCodeAt(0));
|
||||
node.style('shadow-blur', baseBlur * pulse);
|
||||
node.style('shadow-opacity', 0.6 + 0.3 * pulse);
|
||||
node.style('background-opacity', 0.85 + 0.1 * pulse);
|
||||
});
|
||||
}, 80);
|
||||
|
||||
// Neuronale Aktivität simulieren
|
||||
startNeuralActivitySimulation(cy);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,160 +533,91 @@ function applyNeuralNetworkStyle(cy) {
|
||||
* @param {Object} cy - Cytoscape-Instanz
|
||||
*/
|
||||
function startNeuralActivitySimulation(cy) {
|
||||
// Neuronen-Zustand für die Simulation
|
||||
const neuronStates = new Map();
|
||||
if (window.neuralInterval) clearInterval(window.neuralInterval);
|
||||
|
||||
const nodes = cy.nodes();
|
||||
const edges = cy.edges();
|
||||
let currentTime = Date.now();
|
||||
|
||||
// Neuronale Aktivität simulieren
|
||||
function simulateNeuralActivity() {
|
||||
currentTime = Date.now();
|
||||
|
||||
// Initialisieren aller Neuronen-Zustände
|
||||
cy.nodes().forEach(node => {
|
||||
neuronStates.set(node.id(), {
|
||||
potential: Math.random() * 0.3, // Startpotential
|
||||
lastFired: 0, // Zeitpunkt der letzten Aktivierung
|
||||
isRefractory: false, // Refraktärphase
|
||||
refractoryUntil: 0 // Ende der Refraktärphase
|
||||
});
|
||||
// Zufällige Neuronen "feuern" lassen
|
||||
nodes.forEach(node => {
|
||||
const data = node.data();
|
||||
const lastFired = data.lastFired || 0;
|
||||
const timeSinceLastFire = currentTime - lastFired;
|
||||
|
||||
// Prüfen ob Neuron feuern kann (Refraktionsperiode)
|
||||
if (timeSinceLastFire > data.refractionPeriod) {
|
||||
// Zufälliges Feuern basierend auf Aktivität
|
||||
if (Math.random() < data.neuronActivity * 0.1) {
|
||||
fireNeuron(node, true, currentTime);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Neuron feuern lassen
|
||||
function fireNeuron(node, state, currentTime) {
|
||||
const data = node.data();
|
||||
data.lastFired = currentTime;
|
||||
|
||||
// Visuelles Feedback
|
||||
node.style({
|
||||
'background-opacity': 1,
|
||||
'shadow-blur': 25,
|
||||
'shadow-opacity': 0.9
|
||||
});
|
||||
|
||||
// Neuronale Aktivität simulieren
|
||||
function simulateNeuralActivity() {
|
||||
const currentTime = Date.now();
|
||||
const nodes = cy.nodes().toArray();
|
||||
// Nach kurzer Zeit zurück zum Normalzustand
|
||||
setTimeout(() => {
|
||||
node.style({
|
||||
'background-opacity': 0.85,
|
||||
'shadow-blur': 18,
|
||||
'shadow-opacity': 0.6
|
||||
});
|
||||
}, 200);
|
||||
|
||||
// Signal weiterleiten
|
||||
if (state) {
|
||||
propagateSignal(node, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Signal über Kanten weiterleiten
|
||||
function propagateSignal(sourceNode, currentTime) {
|
||||
const outgoingEdges = sourceNode.connectedEdges('out');
|
||||
|
||||
outgoingEdges.forEach(edge => {
|
||||
const targetNode = edge.target();
|
||||
const edgeData = edge.data();
|
||||
const latency = edgeData.latency;
|
||||
|
||||
// Signal mit Verzögerung weiterleiten
|
||||
setTimeout(() => {
|
||||
const targetData = targetNode.data();
|
||||
const timeSinceLastFire = currentTime - (targetData.lastFired || 0);
|
||||
|
||||
// Zufällige Stimulation eines Neurons
|
||||
if (Math.random() > 0.7) {
|
||||
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
|
||||
const randomNode = nodes[randomNodeIndex];
|
||||
|
||||
const state = neuronStates.get(randomNode.id());
|
||||
if (state && !state.isRefractory) {
|
||||
state.potential += 0.5; // Erhöhe das Potential durch externe Stimulation
|
||||
}
|
||||
// Prüfen ob Zielneuron feuern kann
|
||||
if (timeSinceLastFire > targetData.refractionPeriod) {
|
||||
// Signalstärke berechnen
|
||||
const signalStrength = edgeData.strength *
|
||||
edgeData.conductionVelocity *
|
||||
sourceNode.data('neuronActivity');
|
||||
|
||||
// Neuron feuern lassen wenn Signal stark genug
|
||||
if (signalStrength > targetData.threshold) {
|
||||
fireNeuron(targetNode, true, currentTime + latency);
|
||||
}
|
||||
}
|
||||
|
||||
// Neuronen aktualisieren
|
||||
nodes.forEach(node => {
|
||||
const nodeId = node.id();
|
||||
const state = neuronStates.get(nodeId);
|
||||
const threshold = node.data('threshold') || 0.7;
|
||||
const refractoryPeriod = node.data('refractionPeriod') || 1000;
|
||||
|
||||
// Überprüfen, ob die Refraktärphase beendet ist
|
||||
if (state.isRefractory && currentTime >= state.refractoryUntil) {
|
||||
state.isRefractory = false;
|
||||
state.potential = 0.1; // Ruhepotential nach Refraktärphase
|
||||
}
|
||||
|
||||
// Wenn nicht in Refraktärphase und Potential über Schwelle
|
||||
if (!state.isRefractory && state.potential >= threshold) {
|
||||
// Neuron feuert
|
||||
fireNeuron(node, state, currentTime);
|
||||
} else if (!state.isRefractory) {
|
||||
// Potential langsam verlieren, wenn nicht gefeuert wird
|
||||
state.potential *= 0.95;
|
||||
}
|
||||
});
|
||||
|
||||
// Simulation fortsetzen
|
||||
requestAnimationFrame(simulateNeuralActivity);
|
||||
}
|
||||
|
||||
// Neuron "feuern" lassen
|
||||
function fireNeuron(node, state, currentTime) {
|
||||
// Neuron aktivieren
|
||||
node.animate({
|
||||
style: {
|
||||
'background-opacity': 1,
|
||||
'shadow-opacity': 1,
|
||||
'shadow-blur': 25
|
||||
},
|
||||
duration: 300,
|
||||
easing: 'ease-in-cubic',
|
||||
complete: function() {
|
||||
// Zurück zum normalen Zustand
|
||||
node.animate({
|
||||
style: {
|
||||
'background-opacity': 0.85,
|
||||
'shadow-opacity': 0.6,
|
||||
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 5, 15)'
|
||||
},
|
||||
duration: 600,
|
||||
easing: 'ease-out-cubic'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refraktärphase setzen
|
||||
state.isRefractory = true;
|
||||
state.lastFired = currentTime;
|
||||
state.refractoryPeriod = node.data('refractionPeriod') || 1000;
|
||||
state.refractoryUntil = currentTime + state.refractoryPeriod;
|
||||
state.potential = 0; // Potential zurücksetzen
|
||||
|
||||
// Signal über verbundene Synapsen weiterleiten
|
||||
propagateSignal(node, currentTime);
|
||||
}
|
||||
|
||||
// Signal über Synapsen propagieren
|
||||
function propagateSignal(sourceNode, currentTime) {
|
||||
// Verbundene Kanten auswählen
|
||||
const edges = sourceNode.connectedEdges().filter(edge =>
|
||||
edge.source().id() === sourceNode.id() // Nur ausgehende Kanten
|
||||
);
|
||||
|
||||
// Durch alle Kanten iterieren
|
||||
edges.forEach(edge => {
|
||||
// Signalverzögerung basierend auf synaptischen Eigenschaften
|
||||
const latency = edge.data('latency') || 100;
|
||||
const strength = edge.data('strength') || 0.5;
|
||||
|
||||
// Signal entlang der Kante senden
|
||||
setTimeout(() => {
|
||||
edge.animate({
|
||||
style: {
|
||||
'line-color': '#a78bfa',
|
||||
'line-opacity': 0.9,
|
||||
'width': 2.5
|
||||
},
|
||||
duration: 200,
|
||||
easing: 'ease-in',
|
||||
complete: function() {
|
||||
// Kante zurücksetzen
|
||||
edge.animate({
|
||||
style: {
|
||||
'line-color': '#8a8aaa',
|
||||
'line-opacity': 'mapData(strength, 0.2, 0.8, 0.4, 0.7)',
|
||||
'width': 'mapData(strength, 0.2, 0.8, 0.7, 2)'
|
||||
},
|
||||
duration: 400,
|
||||
easing: 'ease-out'
|
||||
});
|
||||
|
||||
// Zielknoten potenzial erhöhen
|
||||
const targetNode = edge.target();
|
||||
const targetState = neuronStates.get(targetNode.id());
|
||||
|
||||
if (targetState && !targetState.isRefractory) {
|
||||
// Potentialzunahme basierend auf synaptischer Stärke
|
||||
targetState.potential += strength * 0.4;
|
||||
|
||||
// Subtile Anzeige der Potenzialänderung
|
||||
targetNode.animate({
|
||||
style: {
|
||||
'background-opacity': Math.min(1, 0.85 + (strength * 0.2)),
|
||||
'shadow-opacity': Math.min(1, 0.6 + (strength * 0.3)),
|
||||
'shadow-blur': Math.min(25, 10 + (strength * 15))
|
||||
},
|
||||
duration: 300,
|
||||
easing: 'ease-in-out'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, latency);
|
||||
});
|
||||
}
|
||||
|
||||
// Starte die Simulation
|
||||
simulateNeuralActivity();
|
||||
}, latency);
|
||||
});
|
||||
}
|
||||
|
||||
// Simulation starten
|
||||
window.neuralInterval = setInterval(simulateNeuralActivity, 100);
|
||||
}
|
||||
|
||||
// Hilfe-Funktion zum Hinzufügen eines Flash-Hinweises
|
||||
|
||||
@@ -172,57 +172,63 @@
|
||||
|
||||
<!-- Kontrollpanel -->
|
||||
<div class="control-panel">
|
||||
<button class="control-button" id="zoom-in">
|
||||
<i class="fas fa-search-plus"></i> Einzoomen
|
||||
<button id="zoomIn" class="control-button">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
<span>Vergrößern</span>
|
||||
</button>
|
||||
<button class="control-button" id="zoom-out">
|
||||
<i class="fas fa-search-minus"></i> Auszoomen
|
||||
<button id="zoomOut" class="control-button">
|
||||
<i class="fas fa-search-minus"></i>
|
||||
<span>Verkleinern</span>
|
||||
</button>
|
||||
<button class="control-button" id="reset-view">
|
||||
<i class="fas fa-compress-arrows-alt"></i> Ansicht zurücksetzen
|
||||
<button id="resetView" class="control-button">
|
||||
<i class="fas fa-sync"></i>
|
||||
<span>Zurücksetzen</span>
|
||||
</button>
|
||||
<button class="control-button" id="toggle-legend">
|
||||
<i class="fas fa-layer-group"></i> Legende ein/aus
|
||||
<button id="toggleLegend" class="control-button">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
<span>Legende</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Info-Panel -->
|
||||
<div class="info-panel" id="node-info">
|
||||
<h3 class="info-title">Knoteninformationen</h3>
|
||||
<div class="info-content">
|
||||
<p>Wählen Sie einen Knoten aus, um Details anzuzeigen.</p>
|
||||
</div>
|
||||
<div id="infoPanel" class="info-panel">
|
||||
<h3 class="info-title">Knotendetails</h3>
|
||||
<div class="info-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Kategorie-Legende -->
|
||||
<div class="category-legend">
|
||||
<div id="categoryLegend" class="category-legend">
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #60a5fa;"></span>
|
||||
Philosophie
|
||||
<div class="category-color" style="background-color: #60a5fa;"></div>
|
||||
<span>Philosophie</span>
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #8b5cf6;"></span>
|
||||
Wissenschaft
|
||||
<div class="category-color" style="background-color: #8b5cf6;"></div>
|
||||
<span>Wissenschaft</span>
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #10b981;"></span>
|
||||
Technologie
|
||||
<div class="category-color" style="background-color: #10b981;"></div>
|
||||
<span>Technologie</span>
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #f59e0b;"></span>
|
||||
Künste
|
||||
<div class="category-color" style="background-color: #f59e0b;"></div>
|
||||
<span>Künste</span>
|
||||
</div>
|
||||
<div class="category-item">
|
||||
<span class="category-color" style="background: #ef4444;"></span>
|
||||
Psychologie
|
||||
<div class="category-color" style="background-color: #ef4444;"></div>
|
||||
<span>Psychologie</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
|
||||
<!-- Cytoscape und Erweiterungen -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
|
||||
|
||||
<!-- Unsere JavaScript-Dateien -->
|
||||
<script src="{{ url_for('static', filename='js/mindmap-interaction.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/update_mindmap.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/mindmap-interaction.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user