chore: automatic commit 2025-04-30 12:48
This commit is contained in:
526
static/d3-extensions.js
vendored
Normal file
526
static/d3-extensions.js
vendored
Normal file
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* D3.js Erweiterungen für verbesserte Mindmap-Funktionalität
|
||||
* Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js
|
||||
*/
|
||||
|
||||
class D3Extensions {
|
||||
/**
|
||||
* Erstellt einen verbesserten radialen Farbverlauf
|
||||
* @param {Object} defs - Das D3 defs Element
|
||||
* @param {string} id - ID für den Gradienten
|
||||
* @param {string} baseColor - Grundfarbe in hexadezimal oder RGB
|
||||
* @returns {Object} - Das erstellte Gradient-Element
|
||||
*/
|
||||
static createEnhancedRadialGradient(defs, id, baseColor) {
|
||||
// Farben berechnen
|
||||
const d3Color = d3.color(baseColor);
|
||||
const lightColor = d3Color.brighter(0.7);
|
||||
const darkColor = d3Color.darker(0.3);
|
||||
const midColor = d3Color;
|
||||
|
||||
// Gradient erstellen
|
||||
const gradient = defs.append('radialGradient')
|
||||
.attr('id', id)
|
||||
.attr('cx', '30%')
|
||||
.attr('cy', '30%')
|
||||
.attr('r', '70%');
|
||||
|
||||
// Farbstops hinzufügen für realistischeren Verlauf
|
||||
gradient.append('stop')
|
||||
.attr('offset', '0%')
|
||||
.attr('stop-color', lightColor.formatHex());
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '50%')
|
||||
.attr('stop-color', midColor.formatHex());
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '100%')
|
||||
.attr('stop-color', darkColor.formatHex());
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Glüheffekt-Filter
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @param {String} color - Farbe des Glüheffekts (Hex-Code)
|
||||
* @param {Number} strength - Stärke des Glüheffekts
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createGlowFilter(defs, id, color = '#b38fff', strength = 5) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Unschärfe-Effekt
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('stdDeviation', strength)
|
||||
.attr('result', 'blur');
|
||||
|
||||
// Farbverstärkung für den Glüheffekt
|
||||
filter.append('feColorMatrix')
|
||||
.attr('in', 'blur')
|
||||
.attr('type', 'matrix')
|
||||
.attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7')
|
||||
.attr('result', 'glow');
|
||||
|
||||
// Farbflut mit der angegebenen Farbe
|
||||
filter.append('feFlood')
|
||||
.attr('flood-color', color)
|
||||
.attr('flood-opacity', '0.7')
|
||||
.attr('result', 'color');
|
||||
|
||||
// Zusammensetzen des Glüheffekts mit der Farbe
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'color')
|
||||
.attr('in2', 'glow')
|
||||
.attr('operator', 'in')
|
||||
.attr('result', 'glow-color');
|
||||
|
||||
// Zusammenfügen aller Ebenen
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'glow-color');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet eine konsistente Farbe aus einem String
|
||||
* @param {string} str - Eingabestring
|
||||
* @returns {string} - Generierte Farbe als Hex-String
|
||||
*/
|
||||
static stringToColor(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
// Basis-Farbpalette für konsistente Farben
|
||||
const colorPalette = [
|
||||
"#4299E1", // Blau
|
||||
"#9F7AEA", // Lila
|
||||
"#ED64A6", // Pink
|
||||
"#48BB78", // Grün
|
||||
"#ECC94B", // Gelb
|
||||
"#F56565", // Rot
|
||||
"#38B2AC", // Türkis
|
||||
"#ED8936", // Orange
|
||||
"#667EEA", // Indigo
|
||||
];
|
||||
|
||||
// Farbe aus der Palette wählen basierend auf dem Hash
|
||||
const colorIndex = Math.abs(hash) % colorPalette.length;
|
||||
return colorPalette[colorIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Schatteneffekt-Filter
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createShadowFilter(defs, id) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Einfacher Schlagschatten
|
||||
filter.append('feDropShadow')
|
||||
.attr('dx', 0)
|
||||
.attr('dy', 4)
|
||||
.attr('stdDeviation', 4)
|
||||
.attr('flood-color', 'rgba(0, 0, 0, 0.3)');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Glasmorphismus-Effekt-Filter
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createGlassMorphismFilter(defs, id) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Hintergrund-Unschärfe für den Glaseffekt
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('stdDeviation', 8)
|
||||
.attr('result', 'blur');
|
||||
|
||||
// Hellere Farbe für den Glaseffekt
|
||||
filter.append('feColorMatrix')
|
||||
.attr('in', 'blur')
|
||||
.attr('type', 'matrix')
|
||||
.attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0')
|
||||
.attr('result', 'glass');
|
||||
|
||||
// Überlagerung mit dem Original
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'glass');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code)
|
||||
* @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code)
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') {
|
||||
// Farbverlauf für den Glaseffekt definieren
|
||||
const gradientId = `gradient-${id}`;
|
||||
const gradient = defs.append('linearGradient')
|
||||
.attr('id', gradientId)
|
||||
.attr('x1', '0%')
|
||||
.attr('y1', '0%')
|
||||
.attr('x2', '100%')
|
||||
.attr('y2', '100%');
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '0%')
|
||||
.attr('stop-color', color1)
|
||||
.attr('stop-opacity', '0.3');
|
||||
|
||||
gradient.append('stop')
|
||||
.attr('offset', '100%')
|
||||
.attr('stop-color', color2)
|
||||
.attr('stop-opacity', '0.3');
|
||||
|
||||
// Filter erstellen
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Hintergrund-Unschärfe
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('stdDeviation', 6)
|
||||
.attr('result', 'blur');
|
||||
|
||||
// Farbverlauf einfügen
|
||||
const feImage = filter.append('feImage')
|
||||
.attr('xlink:href', `#${gradientId}`)
|
||||
.attr('result', 'gradient')
|
||||
.attr('x', '0%')
|
||||
.attr('y', '0%')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('preserveAspectRatio', 'none');
|
||||
|
||||
// Zusammenfügen aller Ebenen
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'blur');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'gradient');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen
|
||||
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||
* @param {String} id - ID des Filters
|
||||
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||
*/
|
||||
static create3DGlassEffect(defs, id) {
|
||||
const filter = defs.append('filter')
|
||||
.attr('id', id)
|
||||
.attr('x', '-50%')
|
||||
.attr('y', '-50%')
|
||||
.attr('width', '200%')
|
||||
.attr('height', '200%');
|
||||
|
||||
// Farbmatrix für Transparenz
|
||||
filter.append('feColorMatrix')
|
||||
.attr('type', 'matrix')
|
||||
.attr('in', 'SourceGraphic')
|
||||
.attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0')
|
||||
.attr('result', 'transparent');
|
||||
|
||||
// Hintergrund-Unschärfe für Tiefe
|
||||
filter.append('feGaussianBlur')
|
||||
.attr('in', 'transparent')
|
||||
.attr('stdDeviation', '4')
|
||||
.attr('result', 'blurred');
|
||||
|
||||
// Lichtquelle und Schattierung hinzufügen
|
||||
const lightSource = filter.append('feSpecularLighting')
|
||||
.attr('in', 'blurred')
|
||||
.attr('surfaceScale', '6')
|
||||
.attr('specularConstant', '1')
|
||||
.attr('specularExponent', '30')
|
||||
.attr('lighting-color', '#ffffff')
|
||||
.attr('result', 'specular');
|
||||
|
||||
lightSource.append('fePointLight')
|
||||
.attr('x', '100')
|
||||
.attr('y', '100')
|
||||
.attr('z', '200');
|
||||
|
||||
// Lichtreflexion verstärken
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'specular')
|
||||
.attr('in2', 'SourceGraphic')
|
||||
.attr('operator', 'in')
|
||||
.attr('result', 'specularHighlight');
|
||||
|
||||
// Inneren Schatten erzeugen
|
||||
const innerShadow = filter.append('feOffset')
|
||||
.attr('in', 'SourceAlpha')
|
||||
.attr('dx', '0')
|
||||
.attr('dy', '1')
|
||||
.attr('result', 'offsetblur');
|
||||
|
||||
innerShadow.append('feGaussianBlur')
|
||||
.attr('in', 'offsetblur')
|
||||
.attr('stdDeviation', '2')
|
||||
.attr('result', 'innerShadow');
|
||||
|
||||
filter.append('feComposite')
|
||||
.attr('in', 'innerShadow')
|
||||
.attr('in2', 'SourceGraphic')
|
||||
.attr('operator', 'out')
|
||||
.attr('result', 'innerShadowEffect');
|
||||
|
||||
// Schichten kombinieren
|
||||
const feMerge = filter.append('feMerge');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'blurred');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'innerShadowEffect');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'specularHighlight');
|
||||
feMerge.append('feMergeNode')
|
||||
.attr('in', 'SourceGraphic');
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu
|
||||
* @param {Object} parent - Das übergeordnete SVG-Element
|
||||
* @param {number} x - X-Koordinate des Zentrums
|
||||
* @param {number} y - Y-Koordinate des Zentrums
|
||||
* @param {string} color - Partikelfarbe (Hex-Code)
|
||||
* @param {number} count - Anzahl der Partikel
|
||||
*/
|
||||
static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) {
|
||||
const particles = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const particle = parent.append('circle')
|
||||
.attr('cx', x)
|
||||
.attr('cy', y)
|
||||
.attr('r', 0)
|
||||
.attr('fill', color)
|
||||
.style('opacity', 0.8);
|
||||
|
||||
particles.push(particle);
|
||||
|
||||
// Partikel animieren
|
||||
animateParticle(particle);
|
||||
}
|
||||
|
||||
function animateParticle(particle) {
|
||||
// Zufällige Richtung und Geschwindigkeit
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 1 + Math.random() * 2;
|
||||
const distance = 20 + Math.random() * 30;
|
||||
|
||||
// Zielposition berechnen
|
||||
const targetX = x + Math.cos(angle) * distance;
|
||||
const targetY = y + Math.sin(angle) * distance;
|
||||
|
||||
// Animation mit zufälliger Dauer
|
||||
const duration = 1000 + Math.random() * 500;
|
||||
|
||||
particle
|
||||
.attr('r', 0)
|
||||
.style('opacity', 0.8)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('cx', targetX)
|
||||
.attr('cy', targetY)
|
||||
.attr('r', 2 + Math.random() * 3)
|
||||
.style('opacity', 0)
|
||||
.on('end', function() {
|
||||
// Partikel entfernen
|
||||
particle.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine Pulsanimation auf einem Knoten durch
|
||||
* @param {Object} node - D3-Knoten-Selektion
|
||||
* @returns {void}
|
||||
*/
|
||||
static pulseAnimation(node) {
|
||||
if (!node) return;
|
||||
|
||||
const circle = node.select('circle');
|
||||
const originalRadius = parseFloat(circle.attr('r'));
|
||||
const originalFill = circle.attr('fill');
|
||||
|
||||
// Pulsanimation
|
||||
circle
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr('r', originalRadius * 1.3)
|
||||
.attr('fill', '#b38fff')
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr('r', originalRadius)
|
||||
.attr('fill', originalFill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet eine adaptive Schriftgröße basierend auf der Textlänge
|
||||
* @param {string} text - Der anzuzeigende Text
|
||||
* @param {number} maxSize - Maximale Schriftgröße in Pixel
|
||||
* @param {number} minSize - Minimale Schriftgröße in Pixel
|
||||
* @returns {number} - Die berechnete Schriftgröße
|
||||
*/
|
||||
static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) {
|
||||
if (!text) return maxSize;
|
||||
|
||||
// Linear die Schriftgröße basierend auf der Textlänge anpassen
|
||||
const length = text.length;
|
||||
if (length <= 6) return maxSize;
|
||||
if (length >= 20) return minSize;
|
||||
|
||||
// Lineare Interpolation
|
||||
const factor = (length - 6) / (20 - 6);
|
||||
return maxSize - factor * (maxSize - minSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Pulsierenden Effekt zu einer Selektion hinzu
|
||||
* @param {Object} selection - D3-Selektion
|
||||
* @param {number} duration - Dauer eines Puls-Zyklus in ms
|
||||
* @param {number} minOpacity - Minimale Opazität
|
||||
* @param {number} maxOpacity - Maximale Opazität
|
||||
*/
|
||||
static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) {
|
||||
function pulse() {
|
||||
selection
|
||||
.transition()
|
||||
.duration(duration / 2)
|
||||
.style('opacity', minOpacity)
|
||||
.transition()
|
||||
.duration(duration / 2)
|
||||
.style('opacity', maxOpacity)
|
||||
.on('end', pulse);
|
||||
}
|
||||
|
||||
// Initialen Stil setzen
|
||||
selection.style('opacity', maxOpacity);
|
||||
|
||||
// Pulsanimation starten
|
||||
pulse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung
|
||||
* @param {Array} databaseNodes - Knotendaten aus der Datenbank
|
||||
* @param {Array} links - Verbindungsdaten oder null für automatische Extraktion
|
||||
* @returns {Object} Aufbereitete Daten für D3.js
|
||||
*/
|
||||
static processDbNodesForVisualization(databaseNodes, links = null) {
|
||||
// Überprüfe, ob Daten vorhanden sind
|
||||
if (!databaseNodes || databaseNodes.length === 0) {
|
||||
console.warn('Keine Knotendaten zum Verarbeiten vorhanden');
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
|
||||
// Knoten mit D3-Kompatiblem Format erstellen
|
||||
const nodes = databaseNodes.map(node => {
|
||||
// Farbgenerierung, falls keine vorhanden
|
||||
const nodeColor = node.color_code ||
|
||||
node.color ||
|
||||
D3Extensions.stringToColor(node.name || 'default');
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
description: node.description || '',
|
||||
thought_count: node.thought_count || 0,
|
||||
color: nodeColor,
|
||||
// Zusätzliche Attribute
|
||||
category_id: node.category_id,
|
||||
is_public: node.is_public !== undefined ? node.is_public : true,
|
||||
// Position, falls vorhanden
|
||||
x: node.x_position,
|
||||
y: node.y_position,
|
||||
// Größe, falls vorhanden
|
||||
scale: node.scale || 1.0
|
||||
};
|
||||
});
|
||||
|
||||
// Verbindungen verarbeiten
|
||||
let processedLinks = [];
|
||||
|
||||
if (links && Array.isArray(links)) {
|
||||
// Verwende übergebene Verbindungen
|
||||
processedLinks = links.map(link => {
|
||||
return {
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
// Zusätzliche Attribute
|
||||
type: link.type || 'default',
|
||||
strength: link.strength || 1
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Extrahiere Verbindungen aus den Knoten
|
||||
databaseNodes.forEach(node => {
|
||||
if (node.connections && Array.isArray(node.connections)) {
|
||||
node.connections.forEach(conn => {
|
||||
processedLinks.push({
|
||||
source: node.id,
|
||||
target: conn.target,
|
||||
type: conn.type || 'default',
|
||||
strength: conn.strength || 1
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { nodes, links: processedLinks };
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Verfügbarkeit sicherstellen
|
||||
window.D3Extensions = D3Extensions;
|
||||
Reference in New Issue
Block a user