Files
livedash-node/components/WordCloud.tsx
Kaj Kowalski 93fbb44eec feat: comprehensive Biome linting fixes and code quality improvements
Major code quality overhaul addressing 58% of all linting issues:

• Type Safety Improvements:
  - Replace all any types with proper TypeScript interfaces
  - Fix Map component shadowing (renamed to CountryMap)
  - Add comprehensive custom error classes system
  - Enhance API route type safety

• Accessibility Enhancements:
  - Add explicit button types to all interactive elements
  - Implement useId() hooks for form element accessibility
  - Add SVG title attributes for screen readers
  - Fix static element interactions with keyboard handlers

• React Best Practices:
  - Resolve exhaustive dependencies warnings with useCallback
  - Extract nested component definitions to top level
  - Fix array index keys with proper unique identifiers
  - Improve component organization and prop typing

• Code Organization:
  - Automatic import organization and type import optimization
  - Fix unused function parameters and variables
  - Enhanced error handling with structured error responses
  - Improve component reusability and maintainability

Results: 248 → 104 total issues (58% reduction)
- Fixed all critical type safety and security issues
- Enhanced accessibility compliance significantly
- Improved code maintainability and performance
2025-06-29 07:35:45 +02:00

158 lines
4.2 KiB
TypeScript

"use client";
import cloud, { type Word } from "d3-cloud";
import { select } from "d3-selection";
import { useEffect, useRef, useState } from "react";
interface WordCloudProps {
words: {
text: string;
value: number;
}[];
width?: number;
height?: number;
minWidth?: number;
minHeight?: number;
}
export default function WordCloud({
words,
width: initialWidth = 500,
height: initialHeight = 300,
minWidth = 200,
minHeight = 200,
}: WordCloudProps) {
const svgRef = useRef<SVGSVGElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [isClient, setIsClient] = useState(false);
const [dimensions, setDimensions] = useState({
width: initialWidth,
height: initialHeight,
});
// Set isClient to true on initial render
useEffect(() => {
setIsClient(true);
}, []);
// Add effect to detect container size changes
useEffect(() => {
if (!containerRef.current || !isClient) return;
// Create ResizeObserver to detect size changes
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
// Ensure minimum dimensions
const newWidth = Math.max(width, minWidth);
const newHeight = Math.max(height, minHeight);
setDimensions({ width: newWidth, height: newHeight });
}
});
// Start observing the container
resizeObserver.observe(containerRef.current);
// Cleanup
return () => {
resizeObserver.disconnect();
};
}, [isClient, minWidth, minHeight]);
// Effect to render the word cloud whenever dimensions or words change
useEffect(() => {
if (!svgRef.current || !isClient || !words.length) return;
const svg = select(svgRef.current);
svg.selectAll("*").remove(); // Clear previous cloud
// Find the max value for proper scaling
const maxValue = Math.max(...words.map((w) => w.value || 1));
// Configure the layout
const layout = cloud()
.size([dimensions.width, dimensions.height])
.words(
words.map((d) => ({
text: d.text,
size: 10 + (d.value * 90) / maxValue, // Scale from 10 to 100 based on value
}))
)
.padding(5)
.rotate(() => (~~(Math.random() * 6) - 3) * 15) // Rotate between -45 and 45 degrees
.fontSize((d: Word) => d.size || 10)
.on("end", draw);
layout.start();
function draw(words: Word[]) {
svg
.append("g")
.attr(
"transform",
`translate(${dimensions.width / 2},${dimensions.height / 2})`
)
.selectAll("text")
.data(words)
.enter()
.append("text")
.style("font-size", (d: Word) => `${d.size || 10}px`)
.style("font-family", "Inter, Arial, sans-serif")
.style("fill", () => {
// Create a nice gradient of colors
const colors = [
"#4299E1", // blue-500
"#3182CE", // blue-600
"#2B6CB0", // blue-700
"#63B3ED", // blue-400
"#90CDF4", // blue-300
"#38B2AC", // teal-500
"#4FD1C5", // teal-400
];
return colors[Math.floor(Math.random() * colors.length)];
})
.style("cursor", "pointer")
.attr("text-anchor", "middle")
.attr(
"transform",
(d: Word) =>
`translate(${d.x || 0},${d.y || 0}) rotate(${d.rotate || 0})`
)
.text((d: Word) => d.text || "");
}
// Cleanup function
return () => {
svg.selectAll("*").remove();
};
}, [words, dimensions, isClient]);
if (!isClient) {
return (
<div className="w-full h-full bg-white flex items-center justify-center">
<span className="text-gray-500">Loading word cloud...</span>
</div>
);
}
return (
<div
ref={containerRef}
className="flex justify-center w-full h-full"
style={{ minHeight: `${minHeight}px` }}
>
<svg
ref={svgRef}
width={dimensions.width}
height={dimensions.height}
className="w-full h-full"
aria-label="Word cloud visualization of categories"
style={{
maxWidth: "100%",
maxHeight: "100%",
}}
/>
</div>
);
}