added d3 and @types/d3 dependencies for enhanced data visualization capabilities in UI development

Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
2025-07-16 22:11:57 +02:00
parent af27b23c2f
commit 73e022212f
3 changed files with 989 additions and 36 deletions

View File

@@ -4,6 +4,240 @@ import HeroSection from '../../components/dreamarchive/HeroSection';
import SectionHeader from '../../components/dreamarchive/SectionHeader';
import DreamyCard from '../../components/dreamarchive/DreamyCard';
import IconWithBackground from '../../components/dreamarchive/IconWithBackground';
import {useEffect, useRef} from 'react';
import * as d3 from 'd3';
// Dream types data with percentages and colors
const dreamTypesData = [
{type: "Angstträume", percentage: 34, color: "#ef4444", darkColor: "#dc2626", note: "Global durchschnittlich"},
{type: "Soziale Träume", percentage: 28, color: "#3b82f6", darkColor: "#2563eb", note: "Kulturelle Variationen"},
{type: "Sexuelle Träume", percentage: 12, color: "#ec4899", darkColor: "#db2777", note: "Altersabhängig"},
{type: "Flugträume", percentage: 18, color: "#a855f7", darkColor: "#9333ea", note: "Persönlichkeitsabhängig"},
{type: "Verfolgungsträume", percentage: 8, color: "#f97316", darkColor: "#ea580c", note: "Stresskorreliert"}
];
// D3.js Pie Chart Component
function D3PieChart() {
const svgRef = useRef<SVGSVGElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
useEffect(() => {
if (!svgRef.current || !tooltipRef.current || !containerRef.current) return;
// Clear any existing SVG content
d3.select(svgRef.current).selectAll("*").remove();
// Set up dimensions
const width = svgRef.current.clientWidth;
const height = svgRef.current.clientHeight;
const margin = 10;
const radius = Math.min(width, height) / 2 - margin;
// Create SVG
const svg = d3.select(svgRef.current)
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
// Create pie generator
const pie = d3.pie<typeof dreamTypesData[0]>()
.value(d => d.percentage)
.sort(null);
// Create arc generator
const arc = d3.arc<d3.PieArcDatum<typeof dreamTypesData[0]>>()
.innerRadius(0)
.outerRadius(radius);
// Get tooltip element
const tooltip = d3.select(tooltipRef.current);
// Function to show tooltip
const showTooltip = (event: MouseEvent | TouchEvent, d: d3.PieArcDatum<typeof dreamTypesData[0]>) => {
const data = d.data;
// Set tooltip content
tooltip.html(`
<div class="font-bold">${data.type}</div>
<div>${data.percentage}%</div>
<div class="text-xs opacity-75">${data.note}</div>
`);
// Make tooltip visible
tooltip
.style("opacity", 1)
.style("visibility", "visible");
// Position tooltip
const containerRect = containerRef.current!.getBoundingClientRect();
let x, y;
if (event instanceof MouseEvent) {
x = event.clientX - containerRect.left;
y = event.clientY - containerRect.top;
} else {
// TouchEvent
const touch = (event as TouchEvent).touches[0];
x = touch.clientX - containerRect.left;
y = touch.clientY - containerRect.top;
}
// Adjust position to avoid going off the container
const tooltipRect = tooltipRef.current!.getBoundingClientRect();
if (x + tooltipRect.width > containerRect.width) {
x = x - tooltipRect.width;
}
if (y + tooltipRect.height > containerRect.height) {
y = y - tooltipRect.height;
}
tooltip
.style("left", `${x + 10}px`)
.style("top", `${y + 10}px`);
};
// Function to hide tooltip
const hideTooltip = () => {
tooltip
.style("opacity", 0)
.style("visibility", "hidden");
};
// Create pie chart with event handlers
svg.selectAll("path")
.data(pie(dreamTypesData))
.enter()
.append("path")
.attr("d", arc)
.attr("fill", d => isDarkMode ? d.data.darkColor : d.data.color)
.attr("stroke", "white")
.style("stroke-width", "1px")
.style("cursor", "pointer")
// Desktop hover events
.on("mouseover", function (event, d) {
// Calculate centroid for this arc to ensure zoom is centered properly
const centroid = arc.centroid(d);
const x = centroid[0];
const y = centroid[1];
d3.select(this).transition().duration(200)
.attr("opacity", 0.8)
.attr("stroke-width", "2px")
// Apply zoom transformation centered on the arc's centroid
.attr("transform", `translate(${x},${y}) scale(1.05) translate(${-x},${-y})`);
showTooltip(event, d);
})
.on("mouseout", function () {
d3.select(this).transition().duration(200)
.attr("opacity", 1)
.attr("stroke-width", "1px")
.attr("transform", null); // Completely remove the transform to reset zoom
hideTooltip();
})
// Mobile/touch events
.on("touchstart", function (event, d) {
// Stop propagation to prevent document touchstart from firing
event.stopPropagation();
// Prevent default to avoid any browser handling
event.preventDefault();
// Store a reference to the element without using 'this'
const element = event.currentTarget;
// Calculate centroid for this arc to ensure zoom is centered properly
const centroid = arc.centroid(d);
const x = centroid[0];
const y = centroid[1];
// Visual feedback
d3.select(element).transition().duration(200)
.attr("opacity", 0.8)
.attr("stroke-width", "2px")
// Apply zoom transformation centered on the arc's centroid
.attr("transform", `translate(${x},${y}) scale(1.05) translate(${-x},${-y})`);
// Show tooltip
showTooltip(event, d);
// Add a temporary touchend handler to reset the visual state
// but keep the tooltip visible
document.addEventListener("touchend", function () {
d3.select(element).transition().duration(200)
.attr("opacity", 1)
.attr("stroke-width", "1px")
.attr("transform", null); // Completely remove the transform to reset zoom
}, {once: true});
})
.on("click", function (event, d) {
event.stopPropagation();
showTooltip(event, d);
});
// Add center circle
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", radius * 0.4)
.attr("fill", isDarkMode ? "#1f2937" : "#ffffff")
.attr("stroke", isDarkMode ? "#374151" : "#e5e7eb")
.style("stroke-width", "1px");
// Add center text
svg.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("class", "text-sm font-bold")
.attr("fill", isDarkMode ? "#e5e7eb" : "#1f2937")
.text("100%");
// Create a function to handle document touchstart that doesn't immediately hide the tooltip
const handleDocumentTouchStart = (event: TouchEvent) => {
// Only hide the tooltip if the touch is outside the pie chart segments
const paths = svg.selectAll("path").nodes();
const touchTarget = document.elementFromPoint(
event.touches[0].clientX,
event.touches[0].clientY
);
// Don't hide if touching a pie segment
if (paths.some(path => path === touchTarget)) {
return;
}
hideTooltip();
};
// Add click event to document to hide tooltip when clicking outside
document.addEventListener("click", hideTooltip);
document.addEventListener("touchstart", handleDocumentTouchStart);
// Cleanup event listeners on unmount
return () => {
document.removeEventListener("click", hideTooltip);
document.removeEventListener("touchstart", handleDocumentTouchStart);
};
}, [isDarkMode]);
return (
<div ref={containerRef} className="relative w-full aspect-square max-w-xs mx-auto">
<svg ref={svgRef} className="w-full h-full"></svg>
<div
ref={tooltipRef}
className="absolute pointer-events-none bg-white dark:bg-gray-800 p-2 rounded shadow-lg text-sm z-10 transition-opacity duration-200"
style={{
opacity: 0,
visibility: 'hidden',
maxWidth: '150px',
border: '1px solid',
borderColor: isDarkMode ? 'rgba(75, 85, 99, 0.5)' : 'rgba(229, 231, 235, 0.5)'
}}
></div>
</div>
);
}
export default function Technology() {
return (<div className="p-4 pt-24 pb-20 max-w-6xl mx-auto relative overflow-hidden">
@@ -116,42 +350,8 @@ export default function Technology() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
{/* Pie Chart Visualization (Mock) */}
<div className="relative w-full aspect-square max-w-xs mx-auto">
<div className="absolute inset-0 rounded-full bg-gray-200 dark:bg-gray-700"></div>
{/* Angstträume: 34% */}
<div className="absolute inset-0 rounded-full bg-red-500 dark:bg-red-600" style={{
clipPath: 'polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%, 75% 75%)'
}}></div>
{/* Soziale Träume: 28% */}
<div className="absolute inset-0 rounded-full bg-blue-500 dark:bg-blue-600" style={{
clipPath: 'polygon(50% 50%, 75% 75%, 50% 100%, 0% 100%, 0% 50%)'
}}></div>
{/* Sexuelle Träume: 12% */}
<div className="absolute inset-0 rounded-full bg-pink-500 dark:bg-pink-600" style={{
clipPath: 'polygon(50% 50%, 0% 50%, 0% 0%, 25% 0%)'
}}></div>
{/* Flugträume: 18% */}
<div className="absolute inset-0 rounded-full bg-purple-500 dark:bg-purple-600" style={{
clipPath: 'polygon(50% 50%, 25% 0%, 50% 0%)'
}}></div>
{/* Verfolgungsträume: 8% */}
<div className="absolute inset-0 rounded-full bg-orange-500 dark:bg-orange-600" style={{
clipPath: 'polygon(50% 50%, 50% 100%, 25% 100%)'
}}></div>
<div className="absolute inset-0 flex items-center justify-center">
<div
className="w-16 h-16 rounded-full bg-white dark:bg-gray-900 flex items-center justify-center text-sm font-bold">
100%
</div>
</div>
</div>
{/* D3.js Pie Chart */}
<D3PieChart/>
</div>
<div>