/* Animated delivery spine. One dot per programme on a true year axis, an accumulation band that only ever grows, and a 30-second playhead that names the flagship it is crossing. Static and complete under reduced motion. */ const SP = window.SX_DATA; const SP_FROM = 1999, SP_TO = 2026; const LAYERS_OF = { "ERP & billing": ["core", "data", "apps"], "Data platforms": ["data", "apps"], "Cloud & DevOps": ["apps"], "Industrial IoT & edge": ["edge", "data", "apps"], "Energy-tech": ["edge", "data", "core"], "Agri-tech": ["edge", "data", "apps"], "Applied ML": ["ml", "data"], "Computer vision": ["ml", "edge", "apps"], "Document intelligence": ["agents", "data", "apps"], "AI agents": ["agents", "data", "apps"], }; const SP_TIER_R = { flagship: 11, deployment: 6.5, delivery: 4.2 }; const SP_TIER_W = { flagship: 3, deployment: 2, delivery: 1 }; const NODE_CAP = 4; const spYear = (p) => +String(p.year || "").slice(0, 4) || null; function buildSpine(rows, W) { const left = 132, right = W - 28; const years = []; for (let y = SP_FROM; y <= SP_TO; y++) years.push(y); const step = (right - left) / years.length; const x = (y) => left + (y - SP_FROM) * step + step / 2; const nodeFloor = 196; const nodes = [], chips = []; years.forEach((y) => { const inYear = rows.filter((p) => spYear(p) === y).sort((a, b) => SP_TIER_R[b.tier] - SP_TIER_R[a.tier]); let cursor = nodeFloor; inYear.slice(0, NODE_CAP).forEach((p) => { const r = SP_TIER_R[p.tier]; cursor -= r + 3; nodes.push({ p, year: y, x: x(y), y: cursor, r }); cursor -= r + 4; }); if (inYear.length > NODE_CAP) chips.push({ year: y, x: x(y), y: cursor - 6, count: inYear.length - NODE_CAP, all: inYear }); }); const axisY = 226, bandTop = 262, rowH = 16, gap = 2; const rowY = {}; SP.layers.forEach((l, i) => { rowY[l.id] = bandTop + (SP.layers.length - 1 - i) * (rowH + gap); }); const cells = [], ghosts = []; const reached = {}; years.forEach((y) => { const active = new Set(); rows.forEach((p) => { const py = spYear(p); if (!py) return; if (py <= y && y <= py + 2) (LAYERS_OF[p.practice] || []).forEach((t) => active.add(t)); if (py <= y) (LAYERS_OF[p.practice] || []).forEach((t) => { if (reached[t] === undefined) reached[t] = y; }); }); SP.layers.forEach((l) => { const cx = x(y) - step / 2 + 1, w = step - 2; if (active.has(l.id)) cells.push({ key: y + l.id, x: cx, y: rowY[l.id], w, h: rowH, year: y }); else if (reached[l.id] !== undefined && reached[l.id] <= y) ghosts.push({ key: y + l.id, x: cx, y: rowY[l.id], w, h: rowH, year: y }); }); }); const wTop = 34, wBottom = 74, wMax = 5; let wPath = ""; years.forEach((y, i) => { const v = rows.filter((p) => spYear(p) === y).reduce((s, p) => s + SP_TIER_W[p.tier], 0); const yy = Math.round((wBottom - (Math.min(v, wMax) / wMax) * (wBottom - wTop)) * 10) / 10; wPath += (i === 0 ? `M${Math.round(x(y) - step / 2)},${yy}` : `L${Math.round(x(y) - step / 2)},${yy}`) + `L${Math.round(x(y) + step / 2)},${yy}`; }); const wArea = `M${left},${wBottom}` + wPath.replace(/^M/, "L") + `L${Math.round(right)},${wBottom}z`; return { years, step, x, left, right, nodes, chips, cells, ghosts, rowY, axisY, bandTop, wPath, wArea, wBottom, wTop, bandBottom: bandTop + SP.layers.length * (rowH + gap) - gap }; } /** Phone layout: the same record as year rows, newest first, with era headers and a per-year band strip. The playhead highlights the year it is on. */ function VerticalSpine({ rows, pyInt }) { const years = []; for (let y = SP_TO; y >= SP_FROM; y--) years.push(y); return (
{years.map((y) => { const era = SP.eras.find((e) => { const [a, b] = e.label.split("–").map(Number); return y >= a && y <= b; }); const [, eraEnd] = era ? era.label.split("–").map(Number) : []; const inYear = rows.filter((p) => spYear(p) === y); const active = new Set(), reach = new Set(); rows.forEach((p) => { const py2 = spYear(p); if (py2 <= y && y <= py2 + 2) (LAYERS_OF[p.practice] || []).forEach((t) => active.add(t)); if (py2 <= y) (LAYERS_OF[p.practice] || []).forEach((t) => reach.add(t)); }); return (
{y === SP_TO || y === eraEnd ? (

{era.title}

{era.label}

) : null}
{SP.layers.slice().reverse().map((l) => ( ))} {y}
{inYear.map((p) => ( {p.short} {p.title} ))}
); })}
); } function AnimatedSpine({ mobile }) { const rows = React.useMemo(() => SP.projects.filter(spYear), []); const wrapRef = React.useRef(null); const [W, setW] = React.useState(mobile ? 700 : 1180); const [playing, setPlaying] = React.useState(false); const [year, setYear] = React.useState(SP_TO); const [hover, setHover] = React.useState(null); const [chip, setChip] = React.useState(null); const raf = React.useRef(0), t0 = React.useRef(0); React.useEffect(() => { const el = wrapRef.current; if (!el) return; const ro = new ResizeObserver(() => setW(Math.max(680, el.clientWidth))); ro.observe(el); setW(Math.max(680, el.clientWidth)); return () => ro.disconnect(); }, []); React.useEffect(() => { if (!playing) { cancelAnimationFrame(raf.current); return; } t0.current = 0; const tick = (ts) => { if (!t0.current) t0.current = ts; const f = ((ts - t0.current) / 30000) % 1; setYear(SP_FROM + f * (SP_TO - SP_FROM)); raf.current = requestAnimationFrame(tick); }; raf.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf.current); }, [playing]); const g = buildSpine(rows, W); const H = g.bandBottom + 56; const py = year, pyInt = Math.round(year); const era = SP.eras.find((e) => { const [a, b] = e.label.split("–").map(Number); return pyInt >= a && pyInt <= b; }) || SP.eras[SP.eras.length - 1]; const landed = rows.filter((p) => spYear(p) === pyInt); const activeLayers = new Set(), reachedLayers = new Set(); rows.forEach((p) => { const y = spYear(p); if (y <= pyInt && pyInt <= y + 2) (LAYERS_OF[p.practice] || []).forEach((t) => activeLayers.add(t)); if (y <= pyInt) (LAYERS_OF[p.practice] || []).forEach((t) => reachedLayers.add(t)); }); const flagLabels = playing ? g.nodes.filter((n) => n.p.tier === "flagship" && py >= n.year && py < n.year + 2.2) : hover ? [] : g.nodes.filter((n) => n.p.slug === "lesco"); const setFromX = (clientX, el) => { const box = el.ownerSVGElement.getBoundingClientRect(); const sx = (clientX - box.left) * (W / box.width); const f = (sx - g.left) / (g.right - g.left); setPlaying(false); setYear(Math.round(SP_FROM + Math.min(1, Math.max(0, f)) * (SP_TO - SP_FROM))); }; return (
{mobile ? "or scroll the years below" : "or drag across the plot"} {rows.length} programmes · {SP_FROM}–{SP_TO}
{mobile ? ( ) : (
{SP.eras.map((e, i) => { const [a, b] = e.label.split("–").map(Number); const x0 = g.x(a) - g.step / 2, x1 = g.x(Math.min(b, SP_TO)) + g.step / 2; const lit = pyInt >= a && pyInt <= b; const maxChars = Math.floor((x1 - x0 - 12) / 6.2); const short = e.short || e.title.toLowerCase(); const label = short.length > maxChars ? short.split(/[ ,]/)[0].slice(0, Math.max(3, maxChars)) : short; return ( {label} ); })} {g.years.map((y) => ( {y % 5 === 0 && {y}} ))} Weight {g.nodes.map((n) => { const fut = playing && n.year > py; const hov = hover && hover.p.slug === n.p.slug; const r = hov ? n.r * 1.3 : n.r; return ( setHover(n)} onPointerLeave={() => setHover(null)}> ); })} {g.chips.map((c) => ( setChip(c.year)} onPointerLeave={() => setChip(null)} style={{ cursor: "pointer" }}> +{c.count} ))} {flagLabels.map((n, i) => { const flip = n.x > g.right - 200; const ly = n.y - n.r - 14 - i * 15; return ( {n.p.short.toLowerCase()} ); })} {g.ghosts.map((c) => )} {g.cells.filter((c) => !(playing && c.year > py)).map((c) => ( ))} {SP.layers.map((l) => ( {l.label} ))} {pyInt} { e.currentTarget.setPointerCapture(e.pointerId); setFromX(e.clientX, e.currentTarget); }} onPointerMove={(e) => { if (e.buttons) setFromX(e.clientX, e.currentTarget); }} onKeyDown={(e) => { if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); setPlaying(false); setYear(Math.min(SP_TO, Math.max(SP_FROM, pyInt + (e.key === "ArrowRight" ? 1 : -1)))); } }} style={{ cursor: "ew-resize" }} /> {hover && !mobile && (

{hover.p.practice.toLowerCase()} · {hover.p.tier}

{hover.p.client}

{hover.p.title}

{hover.p.year}{hover.p.country ? " · " + hover.p.country.toLowerCase() : ""}

)} {chip && (

{chip} · {g.chips.find((c) => c.year === chip).all.length} programmes

    {g.chips.find((c) => c.year === chip).all.map((p) => (
  • {p.short} — {p.title.toLowerCase()}
  • ))}
)}
)}

at the playhead

{pyInt}

{era.title} · {era.label}

landed that year

    {landed.length ? landed.map((p) => (
  • {p.short} — {p.title.toLowerCase()}
  • )) :
  • nothing new shipped this year — the programmes from the years either side were still running
  • }

capability layers

    {SP.layers.slice().reverse().map((l) => { const on = activeLayers.has(l.id), reach = reachedLayers.has(l.id); return (
  • {l.label}
  • ); })}

Dot size is the weight of the programme. The band underneath fills a layer in the years work touching it landed, and keeps a ghost outline of every layer ever reached — that outline never falls. Delivery years for the enterprise record are being confirmed against contracts.

); } Object.assign(window, { AnimatedSpine, VerticalSpine, LAYERS_OF });