Added Mary Seacole back in

This commit is contained in:
2026-02-19 20:27:06 +00:00
parent 72d159484f
commit edc1327987
7 changed files with 263 additions and 146 deletions
+34 -9
View File
@@ -26,6 +26,10 @@ function hashString(input: string): number {
return Math.abs(hash)
}
function isRoleNode(type: string): boolean {
return type === 'role'
}
function isEntityNode(type: string): boolean {
return type === 'role' || type === 'education'
}
@@ -48,7 +52,8 @@ function getHeight(width: number, containerHeight?: number | null): number {
return 400
}
const roleNodes = constellationNodes.filter(n => (n.type === 'role' || n.type === 'education') && !HIDDEN_ENTITY_IDS.has(n.id))
const roleNodes = constellationNodes.filter(n => n.type === 'role' && !HIDDEN_ENTITY_IDS.has(n.id))
const educationNodes = constellationNodes.filter(n => n.type === 'education' && !HIDDEN_ENTITY_IDS.has(n.id))
export function useForceSimulation(
svgRef: React.RefObject<SVGSVGElement | null>,
@@ -84,7 +89,8 @@ export function useForceSimulation(
svg.selectAll('*').remove()
const years = roleNodes.map(n => fractionalYear(n))
const allEntityNodes = [...roleNodes, ...educationNodes]
const years = allEntityNodes.map(n => fractionalYear(n))
const minYear = Math.min(...years)
const maxYear = Math.max(...years)
@@ -301,6 +307,16 @@ export function useForceSimulation(
const skillZoneLeft = sidePadding + srActive
const skillZoneWidth = skillZoneRight - skillZoneLeft
// Education nodes sit on the left side, timeline-anchored on Y
const educationInitialMap = new Map<string, { x: number; y: number }>()
const eduX = skillZoneLeft + rw / 2 + (isMobile ? 8 : Math.round(12 * sf))
educationNodes.forEach((edu) => {
educationInitialMap.set(edu.id, {
x: eduX,
y: yScale(fractionalYear(edu)),
})
})
// Pre-compute skill homeY and group by role-set to offset overlaps
const skillRoleKey = new Map<string, string>() // skillId -> sorted role key
const skillBaseY = new Map<string, number>() // skillId -> base homeY
@@ -312,7 +328,7 @@ export function useForceSimulation(
skillRoleKey.set(n.id, key)
const positions = roleIds
.map(roleId => roleInitialMap.get(roleId))
.map(roleId => roleInitialMap.get(roleId) ?? educationInitialMap.get(roleId))
.filter(Boolean) as Array<{ x: number; y: number }>
const baseY = positions.length > 0
? positions.reduce((sum, p) => sum + p.y, 0) / positions.length
@@ -336,7 +352,11 @@ export function useForceSimulation(
})
const nodes: SimNode[] = visibleNodeData.map(n => {
if (isEntityNode(n.type)) {
if (n.type === 'education') {
const pos = educationInitialMap.get(n.id)!
return { ...n, x: pos.x, y: pos.y, vx: 0, vy: 0, homeX: pos.x, homeY: pos.y }
}
if (isRoleNode(n.type)) {
const pos = roleInitialMap.get(n.id)!
return { ...n, x: pos.x, y: pos.y, vx: 0, vy: 0, homeX: pos.x, homeY: pos.y }
}
@@ -517,9 +537,9 @@ export function useForceSimulation(
})
})
// Entity connectors to timeline
// Role connectors to timeline (education nodes on left don't connect)
const roleConnectors = connectorGroup.selectAll('line.role-connector')
.data(nodes.filter(n => isEntityNode(n.type)))
.data(nodes.filter(n => isRoleNode(n.type)))
.join('line')
.attr('class', 'role-connector')
.attr('stroke', 'var(--border)')
@@ -539,13 +559,15 @@ export function useForceSimulation(
.id(d => d.id)
.distance(isMobile ? 56 : Math.round(120 * sf))
.strength(d => (d as SimLink).strength * 0.15))
.force('x', d3.forceX<SimNode>(d => d.homeX).strength(d => isEntityNode(d.type) ? 1.0 : 0.6))
.force('x', d3.forceX<SimNode>(d => d.homeX).strength(d =>
isRoleNode(d.type) ? 1.0 : d.type === 'education' ? 0.9 : 0.6
))
.force('y', d3.forceY<SimNode>(d => {
if (isEntityNode(d.type)) {
return yScale(fractionalYear(d))
}
return d.homeY
}).strength(d => isEntityNode(d.type) ? 0.98 : 0.25))
}).strength(d => isRoleNode(d.type) ? 0.98 : d.type === 'education' ? 0.85 : 0.25))
.force('collide', d3.forceCollide<SimNode>(d =>
isEntityNode(d.type) ? Math.max(rw, rh) / 2 + (isMobile ? 8 : Math.round(10 * sf)) : srActive + (isMobile ? 14 : Math.round(16 * sf))
).iterations(3))
@@ -556,9 +578,12 @@ export function useForceSimulation(
const renderTick = () => {
nodes.forEach(d => {
if (isEntityNode(d.type)) {
if (isRoleNode(d.type)) {
d.x = Math.max(rw / 2 + 6, Math.min(axisX - roleGap - rw / 2 + rw / 2, d.x))
d.y = Math.max(rh / 2 + topPadding, Math.min(height - rh / 2 - bottomPadding, d.y))
} else if (d.type === 'education') {
d.x = Math.max(rw / 2 + 6, Math.min(skillZoneRight, d.x))
d.y = Math.max(rh / 2 + topPadding, Math.min(height - rh / 2 - bottomPadding, d.y))
} else {
d.x = Math.max(srActive + 6, Math.min(skillZoneRight, d.x))
d.y = Math.max(srActive + topPadding, Math.min(height - skillBottomPadding, d.y))