diff --git a/Ralph/prd.json b/Ralph/prd.json index e0f6d30..e2a0d1e 100644 --- a/Ralph/prd.json +++ b/Ralph/prd.json @@ -193,7 +193,7 @@ "Verify in browser using dev-browser skill" ], "priority": 11, - "passes": false, + "passes": true, "notes": "The key issue is readability — the current graph is too sparse/faint. Larger nodes, thicker links, and stronger colours will help. The background should provide subtle contrast so the white card surface feels like the graph 'lives somewhere'." }, { diff --git a/Ralph/progress.txt b/Ralph/progress.txt index 0f77ead..588f479 100644 --- a/Ralph/progress.txt +++ b/Ralph/progress.txt @@ -983,3 +983,21 @@ - The `.activity-grid` CSS was orphaned when CareerActivityTile was removed — always check for CSS classes that were only used by deleted components - Only 2 tile files remain in src/components/tiles/: PatientSummaryTile.tsx and ProjectsTile.tsx --- + +## 2026-02-14 - US-011 +- Improved constellation graph visual clarity with larger nodes, thicker links, and better contrast +- Role node radius: 24→30, skill node radius: 10→14, collide radius: 30→36 +- Role label font: 8→10px, skill label font: 9→11px, skill label color darkened (#5B7A78→#4A6B69) +- Link stroke: 1→1.5px, opacity: 0.3→0.45, color: #D4E0DE→#B0C4C0 (darker, more visible) +- Background gradient: center #F0F5F4→#EDF2F1 (slightly darker), edge #FFFFFF→#F7FAF9 (warmer) +- Force simulation: charge -200→-120, link distance 80→65, link strength 0.5→0.6, Y strength 0.3→0.4, skill X pull 0.05→0.08, padding 80→60 +- Hover highlight link width: 2→2.5px, skill scale-up: +3→+4px +- All existing interactions preserved (hover dim/highlight, click, keyboard nav) +- Responsive height tiers unchanged (400/300/250px) +- Files changed: src/components/CareerConstellation.tsx +- **Learnings for future iterations:** + - Reducing charge repulsion (-200→-120) combined with shorter link distance (80→65) makes nodes cluster more tightly, filling the container better + - The `#B0C4C0` link color at 0.45 opacity provides much better visibility than `#D4E0DE` at 0.3 while still being subtle + - Hover reset values must exactly match the initial link styling — always update both together + - Skill label offset (dy) should increase proportionally with radius: 14+14=28 (was 10+12=22) +--- diff --git a/src/components/CareerConstellation.tsx b/src/components/CareerConstellation.tsx index 6960932..4798e2d 100644 --- a/src/components/CareerConstellation.tsx +++ b/src/components/CareerConstellation.tsx @@ -12,9 +12,9 @@ const DESKTOP_HEIGHT = 400 const TABLET_HEIGHT = 300 const MOBILE_HEIGHT = 250 -const ROLE_RADIUS = 24 -const SKILL_RADIUS = 10 -const COLLIDE_RADIUS = 30 +const ROLE_RADIUS = 30 +const SKILL_RADIUS = 14 +const COLLIDE_RADIUS = 36 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches @@ -131,8 +131,8 @@ const CareerConstellation: React.FC = ({ .attr('cx', '50%') .attr('cy', '50%') .attr('r', '60%') - gradient.append('stop').attr('offset', '0%').attr('stop-color', '#F0F5F4') - gradient.append('stop').attr('offset', '100%').attr('stop-color', '#FFFFFF') + gradient.append('stop').attr('offset', '0%').attr('stop-color', '#EDF2F1') + gradient.append('stop').attr('offset', '100%').attr('stop-color', '#F7FAF9') // Background rect svg.append('rect') @@ -160,7 +160,7 @@ const CareerConstellation: React.FC = ({ const years = simRoleNodes.map(n => n.startYear ?? 2016) const minYear = Math.min(...years) const maxYear = Math.max(...years) - const padding = 80 + const padding = 60 const xScale = d3.scaleLinear() .domain([minYear, maxYear]) @@ -172,9 +172,9 @@ const CareerConstellation: React.FC = ({ const linkSelection = linkGroup.selectAll('line') .data(links) .join('line') - .attr('stroke', '#D4E0DE') - .attr('stroke-width', 1) - .attr('stroke-opacity', 0.3) + .attr('stroke', '#B0C4C0') + .attr('stroke-width', 1.5) + .attr('stroke-opacity', 0.45) const nodeSelection = nodeGroup.selectAll('g') .data(nodes) @@ -206,7 +206,7 @@ const CareerConstellation: React.FC = ({ .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') .attr('fill', '#FFFFFF') - .attr('font-size', '8') + .attr('font-size', '10') .attr('font-weight', '600') .attr('font-family', 'var(--font-ui)') .attr('pointer-events', 'none') @@ -226,9 +226,9 @@ const CareerConstellation: React.FC = ({ .append('text') .attr('class', 'node-label') .attr('text-anchor', 'middle') - .attr('dy', SKILL_RADIUS + 12) - .attr('fill', '#5B7A78') - .attr('font-size', '9') + .attr('dy', SKILL_RADIUS + 14) + .attr('fill', '#4A6B69') + .attr('font-size', '11') .attr('font-family', 'var(--font-geist-mono)') .attr('pointer-events', 'none') .text(d => d.shortLabel ?? d.label) @@ -262,7 +262,7 @@ const CareerConstellation: React.FC = ({ nodeSelection.filter(n => n.type === 'skill' && connected.has(n.id)) .select('.node-circle') .transition().duration(150) - .attr('r', SKILL_RADIUS + 3) + .attr('r', SKILL_RADIUS + 4) } // Brighten connected links, dim others @@ -272,7 +272,7 @@ const CareerConstellation: React.FC = ({ const src = typeof l.source === 'string' ? l.source : (l.source as SimNode).id const tgt = typeof l.target === 'string' ? l.target : (l.target as SimNode).id if (src === d.id || tgt === d.id) return '#0D6E6E' - return '#D4E0DE' + return '#B0C4C0' }) .attr('stroke-opacity', l => { const src = typeof l.source === 'string' ? l.source : (l.source as SimNode).id @@ -283,8 +283,8 @@ const CareerConstellation: React.FC = ({ .attr('stroke-width', l => { const src = typeof l.source === 'string' ? l.source : (l.source as SimNode).id const tgt = typeof l.target === 'string' ? l.target : (l.target as SimNode).id - if (src === d.id || tgt === d.id) return 2 - return 1 + if (src === d.id || tgt === d.id) return 2.5 + return 1.5 }) }) @@ -301,9 +301,9 @@ const CareerConstellation: React.FC = ({ // Reset all links linkSelection - .attr('stroke', '#D4E0DE') - .attr('stroke-width', 1) - .attr('stroke-opacity', 0.3) + .attr('stroke', '#B0C4C0') + .attr('stroke-width', 1.5) + .attr('stroke-opacity', 0.45) }) // Click interactions @@ -317,20 +317,20 @@ const CareerConstellation: React.FC = ({ // Force simulation const simulation = d3.forceSimulation(nodes) - .force('charge', d3.forceManyBody().strength(-200)) + .force('charge', d3.forceManyBody().strength(-120)) .force('link', d3.forceLink(links) .id(d => d.id) - .distance(80) - .strength(d => (d as SimLink).strength * 0.5)) + .distance(65) + .strength(d => (d as SimLink).strength * 0.6)) .force('x', d3.forceX(d => { if (d.type === 'role' && d.startYear != null) { return xScale(d.startYear) } return width / 2 - }).strength(d => d.type === 'role' ? 0.8 : 0.05)) - .force('y', d3.forceY(height / 2).strength(0.3)) + }).strength(d => d.type === 'role' ? 0.8 : 0.08)) + .force('y', d3.forceY(height / 2).strength(0.4)) .force('collide', d3.forceCollide(d => - d.type === 'role' ? COLLIDE_RADIUS : SKILL_RADIUS + 4 + d.type === 'role' ? COLLIDE_RADIUS : SKILL_RADIUS + 6 )) simulationRef.current = simulation