Skip to content

Commit f26a2bd

Browse files
committed
fix knowledge map
1 parent 4b955a8 commit f26a2bd

File tree

6 files changed

+2966
-1901
lines changed

6 files changed

+2966
-1901
lines changed

components/KnowledgeMap.tsx

Lines changed: 109 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface Article {
1717
tags?: string[];
1818
x: number;
1919
y: number;
20+
cluster: number;
2021
}
2122

2223
export default function KnowledgeMap({
@@ -30,13 +31,10 @@ export default function KnowledgeMap({
3031
const [loading, setLoading] = useState(true);
3132
const [error, setError] = useState<string | null>(null);
3233
const [hoveredArticle, setHoveredArticle] = useState<Article | null>(null);
33-
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null);
3434
const [searchQuery, setSearchQuery] = useState("");
3535
const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
36-
const [neighbors] = useState(4);
37-
const [minDist] = useState(0.05);
38-
const [spread] = useState(6.0);
3936
const { theme } = useTheme();
37+
const zoomBehaviorRef = useRef<any>(null);
4038

4139
// Fetch data from static JSON file
4240
const fetchData = async () => {
@@ -76,10 +74,26 @@ export default function KnowledgeMap({
7674
return dot / (magA * magB);
7775
}, []);
7876

79-
// Render
77+
// Generate color palette for clusters
78+
const getClusterColor = useCallback(
79+
(cluster: number, isDark: boolean): string => {
80+
if (cluster === -1) {
81+
// Noise points - use default color
82+
return isDark ? "#91989C" : "#595857";
83+
}
84+
85+
// HSL color palette with good contrast
86+
const hue = (cluster * 137.5) % 360; // Golden angle for good distribution
87+
const saturation = isDark ? 60 : 55;
88+
const lightness = isDark ? 60 : 50;
89+
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
90+
},
91+
[]
92+
);
93+
94+
// Render (drawing only, no dimension changes)
8095
useEffect(() => {
81-
if (!canvasRef.current || !containerRef.current || filtered.length === 0)
82-
return;
96+
if (!canvasRef.current || !containerRef.current) return;
8397

8498
const canvas = canvasRef.current;
8599
const container = containerRef.current;
@@ -88,15 +102,18 @@ export default function KnowledgeMap({
88102

89103
const dpr = window.devicePixelRatio || 1;
90104
const rect = container.getBoundingClientRect();
91-
canvas.width = rect.width * dpr;
92-
canvas.height = rect.height * dpr;
93-
canvas.style.width = `${rect.width}px`;
94-
canvas.style.height = `${rect.height}px`;
95-
ctx.scale(dpr, dpr);
96-
97105
const width = rect.width;
98106
const height = rect.height;
99107

108+
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
109+
ctx.scale(dpr, dpr);
110+
111+
// Clear canvas (transparent to show container background with texture)
112+
ctx.clearRect(0, 0, width, height);
113+
114+
// Early return if no results
115+
if (filtered.length === 0) return;
116+
100117
const xScale = scaleLinear().domain([0, 1000]).range([0, width]);
101118
const yScale = scaleLinear().domain([0, 1000]).range([0, height]);
102119

@@ -116,9 +133,6 @@ export default function KnowledgeMap({
116133
? "rgba(145, 152, 156, 0.08)"
117134
: "rgba(89, 88, 87, 0.08)";
118135

119-
// Clear canvas (transparent to show container background with texture)
120-
ctx.clearRect(0, 0, width, height);
121-
122136
ctx.save();
123137
ctx.translate(transform.x, transform.y);
124138
ctx.scale(transform.k, transform.k);
@@ -151,13 +165,11 @@ export default function KnowledgeMap({
151165
// Opacity varies by word count (longer posts = more opaque)
152166
const baseOpacity = Math.min(0.8, 0.3 + wordCount / 2000);
153167

154-
let color = dot;
168+
// Use cluster color
169+
let color = getClusterColor(article.cluster, isDark);
155170
let opacity = baseOpacity;
156171

157-
if (article.id === selectedArticle?.id) {
158-
color = dotSelected;
159-
opacity = 1;
160-
} else if (article.id === hoveredArticle?.id) {
172+
if (article.id === hoveredArticle?.id) {
161173
color = dotHover;
162174
opacity = 1;
163175
} else if (
@@ -190,17 +202,45 @@ export default function KnowledgeMap({
190202
theme,
191203
transform,
192204
hoveredArticle,
193-
selectedArticle,
194205
searchQuery,
195206
similarity,
207+
getClusterColor,
196208
]);
197209

198-
// Zoom setup
210+
// Refs to avoid stale closures
211+
const transformRef = useRef(transform);
212+
const hoveredArticleRef = useRef(hoveredArticle);
213+
const filteredRef = useRef(filtered);
214+
199215
useEffect(() => {
200-
if (!canvasRef.current || !containerRef.current) return;
216+
transformRef.current = transform;
217+
}, [transform]);
218+
219+
useEffect(() => {
220+
hoveredArticleRef.current = hoveredArticle;
221+
}, [hoveredArticle]);
222+
223+
useEffect(() => {
224+
filteredRef.current = filtered;
225+
}, [filtered]);
226+
227+
// Canvas initialization and zoom setup
228+
useEffect(() => {
229+
if (!canvasRef.current || !containerRef.current || articles.length === 0)
230+
return;
201231

202232
const canvas = canvasRef.current;
233+
const container = containerRef.current;
234+
235+
// Initialize canvas dimensions ONCE
236+
const dpr = window.devicePixelRatio || 1;
237+
const rect = container.getBoundingClientRect();
238+
canvas.width = rect.width * dpr;
239+
canvas.height = rect.height * dpr;
240+
canvas.style.width = `${rect.width}px`;
241+
canvas.style.height = `${rect.height}px`;
203242

243+
// Set up zoom behavior
204244
const zoomBehavior = d3Zoom()
205245
.scaleExtent([0.5, 10])
206246
.on("zoom", (event) => {
@@ -211,30 +251,33 @@ export default function KnowledgeMap({
211251
});
212252
});
213253

254+
zoomBehaviorRef.current = zoomBehavior;
255+
214256
const selection = select(canvas);
215257
selection.call(zoomBehavior as any);
216258

217-
// Prevent default scroll behavior
218-
const preventScroll = (e: WheelEvent) => {
219-
e.preventDefault();
220-
};
221-
222-
canvas.addEventListener("wheel", preventScroll, { passive: false });
259+
// Handle window resize
260+
const handleResize = () => {
261+
const newRect = container.getBoundingClientRect();
262+
canvas.width = newRect.width * dpr;
263+
canvas.height = newRect.height * dpr;
264+
canvas.style.width = `${newRect.width}px`;
265+
canvas.style.height = `${newRect.height}px`;
223266

224-
return () => {
225-
selection.on(".zoom", null);
226-
canvas.removeEventListener("wheel", preventScroll);
267+
// Re-apply zoom behavior after canvas reset
268+
selection.call(zoomBehavior as any);
227269
};
228-
}, []);
229270

230-
// Mouse hover
231-
const handleMouseMove = useCallback(
232-
(e: React.MouseEvent<HTMLCanvasElement>) => {
233-
if (!canvasRef.current || !containerRef.current) return;
271+
window.addEventListener("resize", handleResize);
234272

235-
const rect = canvasRef.current.getBoundingClientRect();
236-
const x = (e.clientX - rect.left - transform.x) / transform.k;
237-
const y = (e.clientY - rect.top - transform.y) / transform.k;
273+
// Handle mouse move for hover
274+
const handleNativeMouseMove = (e: MouseEvent) => {
275+
const rect = canvas.getBoundingClientRect();
276+
const currentTransform = transformRef.current;
277+
const x =
278+
(e.clientX - rect.left - currentTransform.x) / currentTransform.k;
279+
const y =
280+
(e.clientY - rect.top - currentTransform.y) / currentTransform.k;
238281

239282
const width = rect.width;
240283
const height = rect.height;
@@ -244,7 +287,7 @@ export default function KnowledgeMap({
244287
let closest: Article | null = null;
245288
let minDist = 12;
246289

247-
filtered.forEach((article) => {
290+
filteredRef.current.forEach((article) => {
248291
const dx = xScale(article.x) - x;
249292
const dy = yScale(article.y) - y;
250293
const dist = Math.sqrt(dx * dx + dy * dy);
@@ -255,16 +298,26 @@ export default function KnowledgeMap({
255298
});
256299

257300
setHoveredArticle(closest);
258-
},
259-
[filtered, transform]
260-
);
301+
};
261302

262-
// Click to navigate
263-
const handleClick = useCallback(() => {
264-
if (hoveredArticle) {
265-
window.location.href = `/posts/${hoveredArticle.postSlug}`;
266-
}
267-
}, [hoveredArticle]);
303+
// Handle click for navigation
304+
const handleNativeClick = () => {
305+
const currentHovered = hoveredArticleRef.current;
306+
if (currentHovered) {
307+
window.location.href = `/posts/${currentHovered.postSlug}`;
308+
}
309+
};
310+
311+
canvas.addEventListener("mousemove", handleNativeMouseMove);
312+
canvas.addEventListener("click", handleNativeClick);
313+
314+
return () => {
315+
window.removeEventListener("resize", handleResize);
316+
selection.on(".zoom", null);
317+
canvas.removeEventListener("mousemove", handleNativeMouseMove);
318+
canvas.removeEventListener("click", handleNativeClick);
319+
};
320+
}, [articles.length]);
268321

269322
if (loading) {
270323
return <UMAPLoader className={className} />;
@@ -287,28 +340,26 @@ export default function KnowledgeMap({
287340
}}
288341
>
289342
{/* Search */}
290-
<div className="absolute top-4 left-4 z-20">
343+
<div className="absolute top-4 left-4 z-20 pointer-events-none">
291344
<input
292345
type="text"
293346
placeholder="search..."
294347
value={searchQuery}
295348
onChange={(e) => setSearchQuery(e.target.value)}
296-
className="w-40 px-2 py-1 text-xs bg-white/90 dark:bg-gray-900/90 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-700 rounded focus:outline-none placeholder:text-gray-400"
349+
className="w-40 px-2 py-1 text-xs bg-white/90 dark:bg-gray-900/90 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-700 rounded focus:outline-none placeholder:text-gray-400 pointer-events-auto"
297350
/>
298351
</div>
299352

300353
{/* Canvas */}
301354
<canvas
302355
ref={canvasRef}
303-
onMouseMove={handleMouseMove}
304-
onClick={handleClick}
305-
className="cursor-pointer w-full h-full block"
356+
className="cursor-crosshair w-full h-full block"
306357
style={{ background: "transparent" }}
307358
/>
308359

309360
{/* Article Detail on Hover */}
310361
{hoveredArticle && (
311-
<div className="absolute top-4 right-4 z-20 bg-white/95 dark:bg-gray-900/95 p-3 rounded border border-gray-300 dark:border-gray-700 shadow-sm max-w-xs">
362+
<div className="absolute top-4 right-4 z-20 bg-white/95 dark:bg-gray-900/95 p-3 rounded border border-gray-300 dark:border-gray-700 shadow-sm max-w-xs pointer-events-none">
312363
<h3 className="font-semibold text-sm leading-tight mb-2">
313364
{hoveredArticle.postTitle}
314365
</h3>
@@ -343,7 +394,7 @@ export default function KnowledgeMap({
343394
)}
344395

345396
{/* Instructions */}
346-
<div className="absolute bottom-4 left-4 z-10 bg-white/90 dark:bg-gray-900/90 px-3 py-1 rounded border border-gray-300 dark:border-gray-700 text-xs text-gray-500">
397+
<div className="absolute bottom-4 left-4 z-10 bg-white/90 dark:bg-gray-900/90 px-3 py-1 rounded border border-gray-300 dark:border-gray-700 text-xs text-gray-500 pointer-events-none">
347398
scroll to zoom • drag to pan • click to read
348399
</div>
349400
</div>

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"langchain": "^0.0.112",
4545
"markdown-to-jsx": "^7.1.7",
4646
"marked": "^5.1.2",
47+
"ml-kmeans": "^6.0.0",
4748
"next": "^15.0.3",
4849
"next-connect": "^1.0.0",
4950
"next-sitemap": "^4.2.3",

0 commit comments

Comments
 (0)