zeroed-some/bashamole / 0a7ec06

Browse files

refactor constants stable

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0a7ec066b0018e6bef36f4c5ceaa6cc8b727b626
Parents
1d765cf
Tree
6234ef4

1 changed file

StatusFile+-
M frontend/src/components/TreeVisualizer.tsx 183 78
frontend/src/components/TreeVisualizer.tsxmodified
@@ -22,6 +22,94 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
2222
   const containerRef = useRef<HTMLDivElement>(null);
2323
   const previousLocationRef = useRef<string | null>(null);
2424
 
25
+  // Visual configuration constants
26
+  const NODE_CONFIG = {
27
+    sizes: {
28
+      root: { base: 26, hover: 26 },
29
+      player: { base: 24, hover: 17 },
30
+      regular: { base: 22, hover: 17 }
31
+    },
32
+    colors: {
33
+      player: { fill: '#3B82F6', stroke: '#60A5FA' },
34
+      mole: { fill: '#EF4444', stroke: '#F87171' },
35
+      fhs: { fill: '#8B5CF6', stroke: '#ffffff' },
36
+      regular: { fill: '#10B981', stroke: '#ffffff' }
37
+    },
38
+    strokeWidth: { base: 2, hover: 3 },
39
+    glowFilter: 'url(#glow)'
40
+  };
41
+
42
+  const ICON_CONFIG = {
43
+    size: 40,
44
+    offset: -20,
45
+    paths: {
46
+      player: '/player.svg',
47
+      mole: '/mole.svg'
48
+    }
49
+  };
50
+
51
+  const ANIMATION_CONFIG = {
52
+    nodeHover: { duration: 200 },
53
+    navigation: { duration: 750, easing: d3.easeCubicInOut },
54
+    intro: {
55
+      phases: [
56
+        { duration: 1000 }, // Initial zoom
57
+        { duration: 2000, easing: d3.easeCubicInOut }, // Zoom out
58
+        { duration: 1000 }, // Pause
59
+        { duration: 1500, easing: d3.easeCubicInOut } // Zoom to player
60
+      ]
61
+    },
62
+    celebration: { duration: '1s', repeatCount: 'indefinite' }
63
+  };
64
+
65
+  const LAYOUT_CONFIG = {
66
+    nodeSpacing: 120,
67
+    margin: { top: 100, right: 150, bottom: 100, left: 150 },
68
+    viewBoxMultiplier: 2,
69
+    minHeight: 1200,
70
+    grid: { size: 40, strokeColor: '#1f2937' },
71
+    background: { color: '#111827', opacity: 0.95 }
72
+  };
73
+
74
+  const ZOOM_CONFIG = {
75
+    scaleExtent: [0.1, 3] as [number, number],
76
+    defaultScale: 3,
77
+    fullTreeScale: 0.8,
78
+    treePadding: 200,
79
+    nudgeOffset: { x: 0.1, y: 0.2 }
80
+  };
81
+
82
+  const LINK_CONFIG = {
83
+    gradient: {
84
+      id: 'link-gradient-v',
85
+      start: { color: '#4B5563', opacity: 0.6 },
86
+      end: { color: '#6B7280', opacity: 0.3 }
87
+    },
88
+    strokeWidth: 2,
89
+    opacity: 0.8
90
+  };
91
+
92
+  const LABEL_CONFIG = {
93
+    fontSize: 14,
94
+    fontWeight: { base: '500', player: '700' },
95
+    offset: { parent: -32, leaf: 38 },
96
+    colors: { player: '#93C5FD', regular: '#E5E7EB' },
97
+    textShadow: '0 0 4px rgba(0,0,0,0.8)'
98
+  };
99
+
100
+  const GLOW_CONFIG = {
101
+    id: 'glow',
102
+    stdDeviation: 3
103
+  };
104
+
105
+  const CELEBRATION_CONFIG = {
106
+    ring: {
107
+      startRadius: 15,
108
+      endRadius: 30,
109
+      strokeWidth: 3
110
+    }
111
+  };
112
+
25113
   useEffect(() => {
26114
     if (!treeData || !svgRef.current || !containerRef.current) return;
27115
 
@@ -49,11 +137,11 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
49137
     const maxNodesAtLevel = Math.max(...Object.values(levelCounts));
50138
     
51139
     // Dynamic width based on tree structure
52
-    const nodeSpacing = 120; // Increased from 80 for better spacing
53
-    const dynamicWidth = Math.max(maxNodesAtLevel * nodeSpacing, containerWidth * 2); // Increased multiplier
54
-    const margin = { top: 100, right: 150, bottom: 100, left: 150 }; // Increased margins
140
+    const nodeSpacing = LAYOUT_CONFIG.nodeSpacing;
141
+    const dynamicWidth = Math.max(maxNodesAtLevel * nodeSpacing, containerWidth * LAYOUT_CONFIG.viewBoxMultiplier);
142
+    const margin = LAYOUT_CONFIG.margin;
55143
     const width = dynamicWidth;
56
-    const height = Math.max(containerHeight, 1200);
144
+    const height = Math.max(containerHeight, LAYOUT_CONFIG.minHeight);
57145
 
58146
     const svg = d3
59147
       .select(svgRef.current)
@@ -67,21 +155,21 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
67155
     
68156
     const pattern = defs.append('pattern')
69157
       .attr('id', 'grid')
70
-      .attr('width', 40)
71
-      .attr('height', 40)
158
+      .attr('width', LAYOUT_CONFIG.grid.size)
159
+      .attr('height', LAYOUT_CONFIG.grid.size)
72160
       .attr('patternUnits', 'userSpaceOnUse');
73161
     
74162
     pattern.append('path')
75
-      .attr('d', 'M 40 0 L 0 0 0 40')
163
+      .attr('d', `M ${LAYOUT_CONFIG.grid.size} 0 L 0 0 0 ${LAYOUT_CONFIG.grid.size}`)
76164
       .attr('fill', 'none')
77
-      .attr('stroke', '#1f2937')
165
+      .attr('stroke', LAYOUT_CONFIG.grid.strokeColor)
78166
       .attr('stroke-width', '1');
79167
 
80168
     svg.append('rect')
81169
       .attr('width', '100%')
82170
       .attr('height', '100%')
83
-      .attr('fill', '#111827')
84
-      .style('opacity', 0.95);
171
+      .attr('fill', LAYOUT_CONFIG.background.color)
172
+      .style('opacity', LAYOUT_CONFIG.background.opacity);
85173
     
86174
     svg.append('rect')
87175
       .attr('width', '100%')
@@ -139,7 +227,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
139227
     // Create gradient for links
140228
     const linkGradient = defs
141229
       .append('linearGradient')
142
-      .attr('id', 'link-gradient-v')
230
+      .attr('id', LINK_CONFIG.gradient.id)
143231
       .attr('gradientUnits', 'userSpaceOnUse')
144232
       .attr('x1', '0%')
145233
       .attr('y1', '0%')
@@ -148,13 +236,13 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
148236
     
149237
     linkGradient.append('stop')
150238
       .attr('offset', '0%')
151
-      .attr('stop-color', '#4B5563')
152
-      .attr('stop-opacity', 0.6);
239
+      .attr('stop-color', LINK_CONFIG.gradient.start.color)
240
+      .attr('stop-opacity', LINK_CONFIG.gradient.start.opacity);
153241
     
154242
     linkGradient.append('stop')
155243
       .attr('offset', '100%')
156
-      .attr('stop-color', '#6B7280')
157
-      .attr('stop-opacity', 0.3);
244
+      .attr('stop-color', LINK_CONFIG.gradient.end.color)
245
+      .attr('stop-opacity', LINK_CONFIG.gradient.end.opacity);
158246
 
159247
     // Create links with vertical layout
160248
     const link = g
@@ -167,9 +255,9 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
167255
         .x(d => d.x)
168256
         .y(d => d.y))
169257
       .style('fill', 'none')
170
-      .style('stroke', 'url(#link-gradient-v)')
171
-      .style('stroke-width', 2)
172
-      .style('opacity', 0.8);
258
+      .style('stroke', `url(#${LINK_CONFIG.gradient.id})`)
259
+      .style('stroke-width', LINK_CONFIG.strokeWidth)
260
+      .style('opacity', LINK_CONFIG.opacity);
173261
 
174262
     // Create node groups
175263
     const node = g
@@ -182,10 +270,10 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
182270
 
183271
     // Add glow effect for interactive nodes
184272
     const glowFilter = defs.append('filter')
185
-      .attr('id', 'glow');
273
+      .attr('id', GLOW_CONFIG.id);
186274
     
187275
     glowFilter.append('feGaussianBlur')
188
-      .attr('stdDeviation', '3')
276
+      .attr('stdDeviation', GLOW_CONFIG.stdDeviation)
189277
       .attr('result', 'coloredBlur');
190278
     
191279
     const feMerge = glowFilter.append('feMerge');
@@ -196,38 +284,40 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
196284
     node
197285
       .append('circle')
198286
       .attr('r', d => {
199
-        if (d.data.path === '/') return 16;
200
-        if (d.data.path === playerLocation) return 13;
201
-        return 11;
287
+        if (d.data.path === '/') return NODE_CONFIG.sizes.root.base;
288
+        if (d.data.path === playerLocation) return NODE_CONFIG.sizes.player.base;
289
+        return NODE_CONFIG.sizes.regular.base;
202290
       })
203291
       .style('fill', d => {
204
-        if (d.data.path === playerLocation) return '#3B82F6';
205
-        if (d.data.has_mole) return '#EF4444';
206
-        if (d.data.is_fhs) return '#8B5CF6';
207
-        return '#10B981';
292
+        if (d.data.path === playerLocation) return NODE_CONFIG.colors.player.fill;
293
+        if (d.data.has_mole) return NODE_CONFIG.colors.mole.fill;
294
+        if (d.data.is_fhs) return NODE_CONFIG.colors.fhs.fill;
295
+        return NODE_CONFIG.colors.regular.fill;
208296
       })
209297
       .style('stroke', d => {
210
-        if (d.data.path === playerLocation) return '#60A5FA';
211
-        if (d.data.has_mole) return '#F87171';
212
-        return '#ffffff';
298
+        if (d.data.path === playerLocation) return NODE_CONFIG.colors.player.stroke;
299
+        if (d.data.has_mole) return NODE_CONFIG.colors.mole.stroke;
300
+        return NODE_CONFIG.colors.regular.stroke;
213301
       })
214
-      .style('stroke-width', 2)
302
+      .style('stroke-width', NODE_CONFIG.strokeWidth.base)
215303
       .style('cursor', 'pointer')
216
-      .style('filter', d => d.data.path === playerLocation ? 'url(#glow)' : 'none')
304
+      .style('filter', d => d.data.path === playerLocation ? NODE_CONFIG.glowFilter : 'none')
217305
       .style('transition', 'all 0.3s ease')
218306
       .on('mouseover', function(event, d) {
219307
         d3.select(this)
220308
           .transition()
221
-          .duration(200)
222
-          .attr('r', d.data.path === '/' ? 18 : 14)
223
-          .style('stroke-width', 3);
309
+          .duration(ANIMATION_CONFIG.nodeHover.duration)
310
+          .attr('r', d.data.path === '/' ? NODE_CONFIG.sizes.root.hover : NODE_CONFIG.sizes.regular.hover)
311
+          .style('stroke-width', NODE_CONFIG.strokeWidth.hover);
224312
       })
225313
       .on('mouseout', function(event, d) {
226314
         d3.select(this)
227315
           .transition()
228
-          .duration(200)
229
-          .attr('r', d.data.path === '/' ? 16 : d.data.path === playerLocation ? 13 : 11)
230
-          .style('stroke-width', 2);
316
+          .duration(ANIMATION_CONFIG.nodeHover.duration)
317
+          .attr('r', d.data.path === '/' ? NODE_CONFIG.sizes.root.base : 
318
+                    d.data.path === playerLocation ? NODE_CONFIG.sizes.player.base : 
319
+                    NODE_CONFIG.sizes.regular.base)
320
+          .style('stroke-width', NODE_CONFIG.strokeWidth.base);
231321
       })
232322
       .on('click', (event, d) => {
233323
         if (onNodeClick) {
@@ -243,12 +333,12 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
243333
     // Add labels
244334
     node
245335
       .append('text')
246
-      .attr('dy', d => d.children ? -20 : 25)
336
+      .attr('dy', d => d.children ? LABEL_CONFIG.offset.parent : LABEL_CONFIG.offset.leaf)
247337
       .attr('text-anchor', 'middle')
248
-      .style('font-size', '14px')
249
-      .style('font-weight', d => d.data.path === playerLocation ? '700' : '500')
250
-      .style('fill', d => d.data.path === playerLocation ? '#93C5FD' : '#E5E7EB')
251
-      .style('text-shadow', '0 0 4px rgba(0,0,0,0.8)')
338
+      .style('font-size', `${LABEL_CONFIG.fontSize}px`)
339
+      .style('font-weight', d => d.data.path === playerLocation ? LABEL_CONFIG.fontWeight.player : LABEL_CONFIG.fontWeight.base)
340
+      .style('fill', d => d.data.path === playerLocation ? LABEL_CONFIG.colors.player : LABEL_CONFIG.colors.regular)
341
+      .style('text-shadow', LABEL_CONFIG.textShadow)
252342
       .text(d => d.data.name || '/')
253343
       .style('pointer-events', 'none');
254344
 
@@ -262,11 +352,11 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
262352
       // Add player SVG directly on the node
263353
       playerGroup
264354
         .append('image')
265
-        .attr('xlink:href', '/player.svg')
266
-        .attr('width', 20)
267
-        .attr('height', 20)
268
-        .attr('x', -10)
269
-        .attr('y', -10)
355
+        .attr('xlink:href', ICON_CONFIG.paths.player)
356
+        .attr('width', ICON_CONFIG.size)
357
+        .attr('height', ICON_CONFIG.size)
358
+        .attr('x', ICON_CONFIG.offset)
359
+        .attr('y', ICON_CONFIG.offset)
270360
         .style('pointer-events', 'none');
271361
     }
272362
 
@@ -280,17 +370,17 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
280370
       // Add celebration animation ring
281371
       moleGroup
282372
         .append('circle')
283
-        .attr('r', 15)
373
+        .attr('r', CELEBRATION_CONFIG.ring.startRadius)
284374
         .style('fill', 'none')
285
-        .style('stroke', '#EF4444')
286
-        .style('stroke-width', 3)
375
+        .style('stroke', NODE_CONFIG.colors.mole.fill)
376
+        .style('stroke-width', CELEBRATION_CONFIG.ring.strokeWidth)
287377
         .style('opacity', 0)
288378
         .append('animate')
289379
         .attr('attributeName', 'r')
290
-        .attr('from', '15')
291
-        .attr('to', '30')
292
-        .attr('dur', '1s')
293
-        .attr('repeatCount', 'indefinite');
380
+        .attr('from', CELEBRATION_CONFIG.ring.startRadius)
381
+        .attr('to', CELEBRATION_CONFIG.ring.endRadius)
382
+        .attr('dur', ANIMATION_CONFIG.celebration.duration)
383
+        .attr('repeatCount', ANIMATION_CONFIG.celebration.repeatCount);
294384
 
295385
       moleGroup
296386
         .select('circle')
@@ -298,23 +388,23 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
298388
         .attr('attributeName', 'opacity')
299389
         .attr('from', '1')
300390
         .attr('to', '0')
301
-        .attr('dur', '1s')
302
-        .attr('repeatCount', 'indefinite');
391
+        .attr('dur', ANIMATION_CONFIG.celebration.duration)
392
+        .attr('repeatCount', ANIMATION_CONFIG.celebration.repeatCount);
303393
 
304394
       // Add mole SVG directly on the node
305395
       moleGroup
306396
         .append('image')
307
-        .attr('xlink:href', '/mole.svg')
308
-        .attr('width', 20)
309
-        .attr('height', 20)
310
-        .attr('x', -10)
311
-        .attr('y', -10)
397
+        .attr('xlink:href', ICON_CONFIG.paths.mole)
398
+        .attr('width', ICON_CONFIG.size)
399
+        .attr('height', ICON_CONFIG.size)
400
+        .attr('x', ICON_CONFIG.offset)
401
+        .attr('y', ICON_CONFIG.offset)
312402
         .style('pointer-events', 'none');
313403
     }
314404
 
315405
     // Add zoom and pan behavior
316406
     const zoom = d3.zoom<SVGSVGElement, unknown>()
317
-      .scaleExtent([0.1, 3])
407
+      .scaleExtent(ZOOM_CONFIG.scaleExtent)
318408
       .on('zoom', (event) => {
319409
         g.attr('transform', event.transform);
320410
       });
@@ -337,7 +427,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
337427
     // Animated intro sequence using zoom transitions
338428
     if (playIntro && playerNode) {
339429
       // Start zoomed in on root
340
-      const rootTransform = getZoomTransform(treeNodes, 3);
430
+      const rootTransform = getZoomTransform(treeNodes, ZOOM_CONFIG.defaultScale);
341431
       svg.call(zoom.transform, rootTransform);
342432
       
343433
       // Calculate full tree view
@@ -345,40 +435,55 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
345435
       const xExtent = d3.extent(allNodes, d => d.x) as [number, number];
346436
       const yExtent = d3.extent(allNodes, d => d.y) as [number, number];
347437
       
348
-      const treeWidth = xExtent[1] - xExtent[0] + 200;
349
-      const treeHeight = yExtent[1] - yExtent[0] + 200;
438
+      const treeWidth = xExtent[1] - xExtent[0] + ZOOM_CONFIG.treePadding;
439
+      const treeHeight = yExtent[1] - yExtent[0] + ZOOM_CONFIG.treePadding;
350440
       
351441
       const scaleX = (width - margin.left - margin.right) / treeWidth;
352442
       const scaleY = (height - margin.top - margin.bottom) / treeHeight;
353
-      const fullTreeScale = Math.min(scaleX, scaleY, 0.8);
443
+      const fullTreeScale = Math.min(scaleX, scaleY, ZOOM_CONFIG.fullTreeScale);
354444
       
355445
       const treeCenterX = (xExtent[0] + xExtent[1]) / 2;
356446
       const treeCenterY = (yExtent[0] + yExtent[1]) / 2;
357447
       const treeCenter = { x: treeCenterX, y: treeCenterY } as d3.HierarchyPointNode<TreeNode>;
358448
       const fullTreeTransform = getZoomTransform(treeCenter, fullTreeScale);
359449
       
360
-      // Final player position - nudged 10% left and 15% down
361
-      const playerTransform = getZoomTransform(playerNode, 3, 0.1, 0.2);
450
+      // Final player position with nudge offset
451
+      const playerTransform = getZoomTransform(playerNode, ZOOM_CONFIG.defaultScale, 
452
+                                              ZOOM_CONFIG.nudgeOffset.x, 
453
+                                              ZOOM_CONFIG.nudgeOffset.y);
362454
       
363455
       // Animate using zoom transitions
456
+      const phases = ANIMATION_CONFIG.intro.phases;
364457
       svg.transition()
365
-        .duration(1000)
458
+        .duration(phases[0].duration)
366459
         .call(zoom.transform, rootTransform)
367460
         .transition()
368
-        .duration(2000)
369
-        .ease(d3.easeCubicInOut)
461
+        .duration(phases[1].duration)
462
+        .ease(phases[1].easing!)
370463
         .call(zoom.transform, fullTreeTransform)
371464
         .transition()
372
-        .duration(1000)
465
+        .duration(phases[2].duration)
373466
         .call(zoom.transform, fullTreeTransform)
374467
         .transition()
375
-        .duration(1500)
376
-        .ease(d3.easeCubicInOut)
468
+        .duration(phases[3].duration)
469
+        .ease(phases[3].easing!)
377470
         .call(zoom.transform, playerTransform);
378471
     } else if (playerNode) {
379
-      // No intro: directly position on player - nudged 10% left and 15% down
380
-      const playerTransform = getZoomTransform(playerNode, 3, 0.1, 0.2);
381
-      svg.call(zoom.transform, playerTransform);
472
+      // No intro: position on player with nudge offset
473
+      const playerTransform = getZoomTransform(playerNode, ZOOM_CONFIG.defaultScale,
474
+                                              ZOOM_CONFIG.nudgeOffset.x,
475
+                                              ZOOM_CONFIG.nudgeOffset.y);
476
+      
477
+      if (isNavigation) {
478
+        // Smooth transition for navigation moves
479
+        svg.transition()
480
+          .duration(ANIMATION_CONFIG.navigation.duration)
481
+          .ease(ANIMATION_CONFIG.navigation.easing)
482
+          .call(zoom.transform, playerTransform);
483
+      } else {
484
+        // Instant positioning for initial load
485
+        svg.call(zoom.transform, playerTransform);
486
+      }
382487
     }
383488
 
384489
   }, [treeData, playerLocation, onNodeClick, playIntro]);