feat: US-007 - Curved link lines between roles and skills
This commit is contained in:
+1
-1
@@ -138,7 +138,7 @@
|
|||||||
"Verify in browser — links are nearly invisible at rest and clearly trace pathways on hover"
|
"Verify in browser — links are nearly invisible at rest and clearly trace pathways on hover"
|
||||||
],
|
],
|
||||||
"priority": 7,
|
"priority": 7,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": "The linkSelection is created at lines ~340-345. Change from .join('line') to .join('path'). For the curve, generate a simple quadratic or cubic bezier path string in the tick handler: given source (sx,sy) and target (tx,ty), create a path like M sx,sy Q cx,cy tx,ty where cx,cy is a control point offset to create a gentle arc. A simple approach: control point at ((sx+tx)/2, sy) or ((sx+tx)/2, (sy+ty)/2 + offset). Alternatively use d3.linkHorizontal() or d3.linkVertical() which generate smooth curves between two points. The applyGraphHighlight function's link styling (lines ~465-482) needs updating from line attributes to path attributes — but stroke/stroke-opacity/stroke-width work the same on paths. Use the d3-viz skill for implementation."
|
"notes": "The linkSelection is created at lines ~340-345. Change from .join('line') to .join('path'). For the curve, generate a simple quadratic or cubic bezier path string in the tick handler: given source (sx,sy) and target (tx,ty), create a path like M sx,sy Q cx,cy tx,ty where cx,cy is a control point offset to create a gentle arc. A simple approach: control point at ((sx+tx)/2, sy) or ((sx+tx)/2, (sy+ty)/2 + offset). Alternatively use d3.linkHorizontal() or d3.linkVertical() which generate smooth curves between two points. The applyGraphHighlight function's link styling (lines ~465-482) needs updating from line attributes to path attributes — but stroke/stroke-opacity/stroke-width work the same on paths. Use the d3-viz skill for implementation."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
- Bidirectional highlighting uses two independent state vars in DashboardLayout: highlightedNodeId (timeline→graph) and highlightedRoleId (graph→timeline)
|
- Bidirectional highlighting uses two independent state vars in DashboardLayout: highlightedNodeId (timeline→graph) and highlightedRoleId (graph→timeline)
|
||||||
- callbacksRef pattern in CareerConstellation prevents stale closures — always add new callbacks there
|
- callbacksRef pattern in CareerConstellation prevents stale closures — always add new callbacks there
|
||||||
- LastConsultationSubsection is defined inline in DashboardLayout.tsx, not a separate file
|
- LastConsultationSubsection is defined inline in DashboardLayout.tsx, not a separate file
|
||||||
|
- Link lines are `<path>` elements (not `<line>`) using quadratic bezier curves — tick handler sets `d` attr, not x1/y1/x2/y2. CSS transitions handle highlight animations on stroke properties
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2026-02-16 - US-005
|
## 2026-02-16 - US-005
|
||||||
@@ -109,3 +110,17 @@
|
|||||||
- LastConsultationSubsection is defined inline in DashboardLayout.tsx, not as a separate file — props must be threaded through the local function definition
|
- LastConsultationSubsection is defined inline in DashboardLayout.tsx, not as a separate file — props must be threaded through the local function definition
|
||||||
- D3 mouseenter events on SVG `<g>` elements require direct mouse interaction with the SVG, not the React button overlay layer
|
- D3 mouseenter events on SVG `<g>` elements require direct mouse interaction with the SVG, not the React button overlay layer
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-02-16 - US-007
|
||||||
|
- Replaced straight `<line>` elements with curved `<path>` elements for link lines between roles and skills
|
||||||
|
- Link paths use quadratic bezier curves: `M sx,sy Q cx,sy tx,ty` where cx is the horizontal midpoint — creating a gentle arc that exits horizontally from the role node before curving to the skill
|
||||||
|
- Added `fill: none` to paths (required since paths auto-fill unlike lines)
|
||||||
|
- Added CSS transitions on stroke/stroke-opacity/stroke-width (150ms ease) for smooth highlight animations, respecting prefers-reduced-motion
|
||||||
|
- applyGraphHighlight link styling unchanged — stroke/stroke-opacity/stroke-width attributes work identically on `<path>` as on `<line>`
|
||||||
|
- Files changed: src/components/CareerConstellation.tsx, Ralph/prd.json
|
||||||
|
- **Learnings for future iterations:**
|
||||||
|
- When converting `<line>` to `<path>`, always add `fill: none` — SVG paths default to `fill: black` which would cover the curve area
|
||||||
|
- Quadratic bezier with control point at `((sx+tx)/2, sy)` creates a nice horizontal-exit curve from role nodes — the path leaves horizontally then arcs down/up to the skill
|
||||||
|
- CSS transitions work on SVG `<path>` stroke properties, so no D3 `.transition()` needed for link highlight animations (unlike `r` attribute which requires D3 transitions)
|
||||||
|
- The tick handler generates the `d` attribute string directly — simpler than using `d3.line().curve()` since we only need two-point curves
|
||||||
|
---
|
||||||
|
|||||||
@@ -313,12 +313,17 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
const connectorGroup = svg.append('g').attr('class', 'connectors')
|
const connectorGroup = svg.append('g').attr('class', 'connectors')
|
||||||
const nodeGroup = svg.append('g').attr('class', 'nodes')
|
const nodeGroup = svg.append('g').attr('class', 'nodes')
|
||||||
|
|
||||||
const linkSelection = linkGroup.selectAll('line')
|
const linkSelection = linkGroup.selectAll('path')
|
||||||
.data(links)
|
.data(links)
|
||||||
.join('line')
|
.join('path')
|
||||||
|
.attr('fill', 'none')
|
||||||
.attr('stroke', 'var(--border-light)')
|
.attr('stroke', 'var(--border-light)')
|
||||||
.attr('stroke-width', 1)
|
.attr('stroke-width', 1)
|
||||||
.attr('stroke-opacity', 0.08)
|
.attr('stroke-opacity', 0.08)
|
||||||
|
.style('transition', prefersReducedMotion
|
||||||
|
? 'none'
|
||||||
|
: 'stroke 150ms ease, stroke-opacity 150ms ease, stroke-width 150ms ease'
|
||||||
|
)
|
||||||
|
|
||||||
const nodeSelection = nodeGroup.selectAll<SVGGElement, SimNode>('g')
|
const nodeSelection = nodeGroup.selectAll<SVGGElement, SimNode>('g')
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
@@ -573,10 +578,14 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
linkSelection
|
linkSelection
|
||||||
.attr('x1', d => (d.source as SimNode).x)
|
.attr('d', d => {
|
||||||
.attr('y1', d => (d.source as SimNode).y)
|
const sx = (d.source as SimNode).x
|
||||||
.attr('x2', d => (d.target as SimNode).x)
|
const sy = (d.source as SimNode).y
|
||||||
.attr('y2', d => (d.target as SimNode).y)
|
const tx = (d.target as SimNode).x
|
||||||
|
const ty = (d.target as SimNode).y
|
||||||
|
const cx = (sx + tx) / 2
|
||||||
|
return `M${sx},${sy} Q${cx},${sy} ${tx},${ty}`
|
||||||
|
})
|
||||||
|
|
||||||
nodeSelection.attr('transform', d => `translate(${d.x},${d.y})`)
|
nodeSelection.attr('transform', d => `translate(${d.x},${d.y})`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user