From 4052529fd732792af0f7072ffa655b0abb851edb Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:12:38 +0200 Subject: [PATCH 01/21] Fix hero badge mobile overflow, densify the UMAP demo, and redesign the homepage highlight sections The conference-announcement chip lacked min-width: 0 on its flex items, so it couldn't shrink below its unwrapped text width and overflowed the viewport on narrow screens. The interactive UMAP demo used too few, too-tightly-packed points and generic chart-library colors, so it read as a sparse placeholder rather than real data. It now uses more points and the same brand hues as the package tiles below it. The Ecosystem/Mission/Team/References sections were four identical stacked text blocks. They're now a single 2x2 card grid with an icon per topic, consistent with the card styling used elsewhere on the site. --- assets/main.scss | 89 ++++++++++++++++++++++++++++++++++------------ layouts/index.html | 84 +++++++++++++++++++++---------------------- static/main.js | 24 ++++++------- 3 files changed, 120 insertions(+), 77 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index 5089bc0..04d9af9 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -414,6 +414,9 @@ body { .event-chip { width: auto; + max-width: 100%; + // Flex items default to min-width: auto, which pins them to their content's intrinsic width instead of letting them shrink. + min-width: 0; display: inline-flex; align-items: center; background-color: transparent; @@ -429,6 +432,10 @@ body { border-radius: 1.5rem; padding: 0.05rem 0.6rem 0.05rem 0; font-size: 0.7rem; + // Lets the date and message stack instead of forcing one nowrap line into horizontal scroll on narrow viewports. + flex-wrap: wrap; + justify-content: center; + row-gap: 0.35rem; } .event-date { @@ -448,6 +455,9 @@ body { .event-text { position: relative; + // Same min-width: auto fix as .event-chip, this time so the text itself can wrap. + min-width: 0; + white-space: normal; .arrow { display: inline-block; @@ -579,33 +589,66 @@ body { } } } - #mission { - .section-content { - p { - font-size: 1.1rem; + #about-highlights { + .about-card-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + + @media (max-width: 50rem) { + grid-template-columns: 1fr; + gap: 1rem; } } - } - #team { - .section-content { - .section-paragraph { - #team-list { - display: inline; - } - a { - border-bottom: 1px solid $linkunderline; - transition: - border 200ms, - color 200ms; - - &:hover, - &:active { - color: black; - border-bottom: 1px solid black; - } - } + + .about-card { + background-color: $tilebg4; + border: 1px solid $overline; + border-radius: $card-radius; + padding: 1.75rem; + transition: all 200ms ease-in-out; + + &:hover { + border-color: $tilebg2; + box-shadow: 0 0.6rem 1.5rem rgba(0, 0, 0, 0.06); + transform: translateY(-2px); + } + } + + .about-card-icon { + width: 2.75rem; + height: 2.75rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + margin-bottom: 1rem; + font-size: 1.3rem; + background-color: $tilebg; + color: $tiletext; + } + + // Written as a descendant rather than nested under .about-card-icon: `&` there would carry the whole ancestor chain and put .about-card in front of #about-highlights. + @each $hue in $eco-hues { + .about-card[data-hue="#{index($eco-hues, $hue) - 1}"] + .about-card-icon { + background-color: rgba($hue, 0.16); + color: darken($hue, 12%); } } + + .about-card-title { + font-size: 1.2rem; + font-weight: 700; + margin: 0 0 0.5rem; + color: $tiletext; + } + + .about-card-text { + font-size: 1rem; + color: $tiletext2; + margin-bottom: 0.75rem; + } } } } diff --git a/layouts/index.html b/layouts/index.html index c5be35d..a04c539 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -90,51 +90,51 @@

{{ .Title }}

{{ partial "main/packages" . }} -
-
Ecosystem
-
-

- {{ .Params.ecosystem.text | markdownify }} -

- -
Continue to scverse ecosystem
-
-
-
+
+
+
+
+

Ecosystem

+

+ {{ .Params.ecosystem.text | markdownify }} +

+ +
Continue to scverse ecosystem
+
+
-
-
Mission
-
-

- {{ .Params.mission.text | markdownify }} -

- -
Read more about scverse
-
-
-
+
+
+

Mission

+

+ {{ .Params.mission.text | markdownify }} +

+ +
Read more about scverse
+
+
-
-
Team
-
-

- {{ .Params.team.text | markdownify }} -

- -
Learn more about scverse community
-
-
-
+
+
+

Team

+

+ {{ .Params.team.text | markdownify }} +

+ +
Learn more about scverse community
+
+
-
-
References
-
-

- {{ .Params.references.text | markdownify }} -

- -
Find each package's publication
-
+
+
+

References

+

+ {{ .Params.references.text | markdownify }} +

+ +
Find each package's publication
+
+
diff --git a/static/main.js b/static/main.js index f2cc3a2..a2321d5 100644 --- a/static/main.js +++ b/static/main.js @@ -267,16 +267,16 @@ const initInteractiveViz = () => { // Exit early if visualization elements don't exist on this page if (!visualization || !card) return; - // Color clusters for UMAP visualization + // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ - { color: '#4285F4', count: 14, name: 'Cluster A' }, - { color: '#34A853', count: 12, name: 'Cluster B' }, - { color: '#FBBC05', count: 10, name: 'Cluster C' }, - { color: '#EA4335', count: 8, name: 'Cluster D' }, - { color: '#FF00FF', count: 11, name: 'Cluster E' }, - { color: '#00BCD4', count: 9, name: 'Cluster F' }, - { color: '#9C27B0', count: 7, name: 'Cluster G' }, - { color: '#FF9800', count: 13, name: 'Cluster H' } + { color: '#40a9ff', count: 34, name: 'Cluster A' }, + { color: '#4ab274', count: 29, name: 'Cluster B' }, + { color: '#fbb822', count: 26, name: 'Cluster C' }, + { color: '#e5864b', count: 22, name: 'Cluster D' }, + { color: '#da347f', count: 27, name: 'Cluster E' }, + { color: '#969dea', count: 24, name: 'Cluster F' }, + { color: '#de367b', count: 19, name: 'Cluster G' }, + { color: '#6cf1a1', count: 31, name: 'Cluster H' } ]; // 3D tilt effect @@ -329,7 +329,7 @@ const initInteractiveViz = () => { dot.dataset.cluster = cluster.name; dot.dataset.color = cluster.color; - const size = Math.floor(Math.random() * 12) + 8; + const size = Math.floor(Math.random() * 8) + 5; dot.style.width = `${size}px`; dot.style.height = `${size}px`; @@ -341,7 +341,7 @@ const initInteractiveViz = () => { u = u / 6 - 0.5; v = v / 6 - 0.5; - const distance = Math.random() * 70 + 10; + const distance = Math.random() * 130 + 20; const dx = u * distance * 2; const dy = v * distance * 2; const x = centerX + dx; @@ -362,7 +362,7 @@ const initInteractiveViz = () => { setTimeout(() => { dot.style.transform = 'scale(1)'; dot.style.opacity = '1'; - }, i * 50 + Math.random() * 200); + }, i * 18 + Math.random() * 150); } }); From daf36fe828552b6b2269c947a17fe84074cca5a5 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:19:21 +0200 Subject: [PATCH 02/21] Add a CellRank-style trajectory graph to the UMAP demo and bottom-align highlight card links The demo previously only showed a static point cloud. It now draws a curved, arrowed trajectory graph over the cluster centroids that forks into two fates near the end, evoking a CellRank/PAGA fate map instead of a bare scatter plot. The "more" links in the homepage highlight cards followed the paragraph text, so cards with shorter text had their link sitting higher than cards with longer text. They're now pinned to the bottom of each card. --- assets/main.scss | 38 +++++++++++++++++++++++++++ static/main.js | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/assets/main.scss b/assets/main.scss index 04d9af9..34ee088 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -602,6 +602,8 @@ body { } .about-card { + display: flex; + flex-direction: column; background-color: $tilebg4; border: 1px solid $overline; border-radius: $card-radius; @@ -613,6 +615,11 @@ body { box-shadow: 0 0.6rem 1.5rem rgba(0, 0, 0, 0.06); transform: translateY(-2px); } + + // Pins the "more" link to the card's bottom edge regardless of how many lines the paragraph above it takes. + > a { + margin-top: auto; + } } .about-card-icon { @@ -3090,6 +3097,37 @@ body { cursor: pointer; } +.trajectory-svg { + position: absolute; + inset: 0; + overflow: visible; + pointer-events: none; + z-index: 2; + opacity: 0; + animation: trajectory-appear 0.6s ease forwards; +} + +.trajectory-edge { + fill: none; + stroke-width: 2.5px; + stroke-linecap: round; + stroke-dasharray: 7 6; + opacity: 0.6; + animation: trajectory-flow 1.2s linear infinite; +} + +@keyframes trajectory-appear { + to { + opacity: 1; + } +} + +@keyframes trajectory-flow { + to { + stroke-dashoffset: -26; + } +} + .animation-pulse { animation: pulse 1.5s infinite; } diff --git a/static/main.js b/static/main.js index a2321d5..a9b0f72 100644 --- a/static/main.js +++ b/static/main.js @@ -315,13 +315,17 @@ const initInteractiveViz = () => { function generateUMAP() { visualization.querySelectorAll('.dot').forEach(dot => dot.remove()); + const oldTrajectory = visualization.querySelector('.trajectory-svg'); + if (oldTrajectory) oldTrajectory.remove(); const width = visualization.clientWidth; const height = visualization.clientHeight; + const centers = []; colorClusters.forEach(cluster => { const centerX = Math.random() * 0.6 * width + 0.2 * width; const centerY = Math.random() * 0.6 * height + 0.2 * height; + centers.push({ x: centerX, y: centerY, color: cluster.color }); for (let i = 0; i < cluster.count; i++) { const dot = document.createElement('div'); @@ -367,6 +371,69 @@ const initInteractiveViz = () => { }); setupDotInteractions(); + setTimeout(() => drawTrajectory(centers), 850); + } + + // Draws a CellRank/PAGA-style trajectory graph over the clusters: a chain of curved, arrowed edges that forks into two fates at the end, standing in for a real pseudotime computation. + function drawTrajectory(centers) { + const svgNS = 'http://www.w3.org/2000/svg'; + const old = visualization.querySelector('.trajectory-svg'); + if (old) old.remove(); + if (centers.length < 4) return; + + const ordered = [...centers].sort((a, b) => a.x - b.x); + const edges = []; + for (let i = 0; i < ordered.length - 3; i++) { + edges.push([ordered[i], ordered[i + 1]]); + } + const branchFrom = ordered[ordered.length - 3]; + edges.push([branchFrom, ordered[ordered.length - 2]]); + edges.push([branchFrom, ordered[ordered.length - 1]]); + + const svg = document.createElementNS(svgNS, 'svg'); + svg.setAttribute('class', 'trajectory-svg'); + svg.setAttribute('width', visualization.clientWidth); + svg.setAttribute('height', visualization.clientHeight); + + const defs = document.createElementNS(svgNS, 'defs'); + edges.forEach((edge, i) => { + const marker = document.createElementNS(svgNS, 'marker'); + marker.setAttribute('id', `traj-arrow-${i}`); + marker.setAttribute('viewBox', '0 0 10 10'); + marker.setAttribute('refX', '8'); + marker.setAttribute('refY', '5'); + marker.setAttribute('markerWidth', '7'); + marker.setAttribute('markerHeight', '7'); + marker.setAttribute('orient', 'auto-start-reverse'); + const arrowhead = document.createElementNS(svgNS, 'path'); + arrowhead.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z'); + arrowhead.setAttribute('fill', edge[1].color); + marker.appendChild(arrowhead); + defs.appendChild(marker); + }); + svg.appendChild(defs); + + edges.forEach(([from, to], i) => { + const mx = (from.x + to.x) / 2; + const my = (from.y + to.y) / 2; + const dx = to.x - from.x; + const dy = to.y - from.y; + const dist = Math.hypot(dx, dy) || 1; + // Bows the edge outward, like a PAGA/CellRank transition graph, instead of a straight line. + const bow = Math.min(dist * 0.22, 60) * (i % 2 === 0 ? 1 : -1); + const cx = mx - (dy / dist) * bow; + const cy = my + (dx / dist) * bow; + + const path = document.createElementNS(svgNS, 'path'); + path.setAttribute('d', `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`); + path.setAttribute('class', 'trajectory-edge'); + path.setAttribute('stroke', to.color); + path.setAttribute('marker-end', `url(#traj-arrow-${i})`); + path.style.animationDelay = `${i * 0.15}s`; + svg.appendChild(path); + }); + + visualization.appendChild(svg); } function setupDotInteractions() { From 4c35ab7b9e1aebc0bfcc278516ac32eea966dc46 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:28:06 +0200 Subject: [PATCH 03/21] Redo the demo trajectory as a scVelo-style streamline bundle and tighten spacing under it The previous trajectory graph drew one curved edge per cluster transition, which looked like a PAGA abstraction rather than a real velocity field. It's now a bundle of parallel arrowed streamlines per transition, fanned out at the source cluster and converging at the target, matching the many-small-arrows look of scVelo/CellRank plots. #interactive-demo-section's margin-bottom was 15%, stacking on top of the next section's own top margin and leaving a large gap before "Next event". Reduced to a fixed 3rem. --- assets/main.scss | 20 +++++------- static/main.js | 85 ++++++++++++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index 34ee088..0a71911 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -523,7 +523,7 @@ body { // The wide gap under the hero used to live on #packages, which put it // between whatever section came after the demo and the packages. #interactive-demo-section { - margin-bottom: 15%; + margin-bottom: 3rem; } #packages { #packages-list { @@ -3107,24 +3107,20 @@ body { animation: trajectory-appear 0.6s ease forwards; } -.trajectory-edge { +.trajectory-line { fill: none; - stroke-width: 2.5px; + stroke-width: 1px; stroke-linecap: round; - stroke-dasharray: 7 6; - opacity: 0.6; - animation: trajectory-flow 1.2s linear infinite; + opacity: 0.55; } -@keyframes trajectory-appear { - to { - opacity: 1; - } +.trajectory-arrow { + opacity: 0.75; } -@keyframes trajectory-flow { +@keyframes trajectory-appear { to { - stroke-dashoffset: -26; + opacity: 1; } } diff --git a/static/main.js b/static/main.js index a9b0f72..b4609e0 100644 --- a/static/main.js +++ b/static/main.js @@ -325,7 +325,7 @@ const initInteractiveViz = () => { colorClusters.forEach(cluster => { const centerX = Math.random() * 0.6 * width + 0.2 * width; const centerY = Math.random() * 0.6 * height + 0.2 * height; - centers.push({ x: centerX, y: centerY, color: cluster.color }); + centers.push({ x: centerX, y: centerY, color: cluster.color, name: cluster.name }); for (let i = 0; i < cluster.count; i++) { const dot = document.createElement('div'); @@ -374,7 +374,7 @@ const initInteractiveViz = () => { setTimeout(() => drawTrajectory(centers), 850); } - // Draws a CellRank/PAGA-style trajectory graph over the clusters: a chain of curved, arrowed edges that forks into two fates at the end, standing in for a real pseudotime computation. + // Draws a scVelo/CellRank-style velocity field: bundles of parallel arrowed streamlines that fan out from a cluster and converge into the next one, rather than one graph edge between cluster centroids. function drawTrajectory(centers) { const svgNS = 'http://www.w3.org/2000/svg'; const old = visualization.querySelector('.trajectory-svg'); @@ -394,46 +394,59 @@ const initInteractiveViz = () => { svg.setAttribute('class', 'trajectory-svg'); svg.setAttribute('width', visualization.clientWidth); svg.setAttribute('height', visualization.clientHeight); + visualization.appendChild(svg); - const defs = document.createElementNS(svgNS, 'defs'); - edges.forEach((edge, i) => { - const marker = document.createElementNS(svgNS, 'marker'); - marker.setAttribute('id', `traj-arrow-${i}`); - marker.setAttribute('viewBox', '0 0 10 10'); - marker.setAttribute('refX', '8'); - marker.setAttribute('refY', '5'); - marker.setAttribute('markerWidth', '7'); - marker.setAttribute('markerHeight', '7'); - marker.setAttribute('orient', 'auto-start-reverse'); - const arrowhead = document.createElementNS(svgNS, 'path'); - arrowhead.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z'); - arrowhead.setAttribute('fill', edge[1].color); - marker.appendChild(arrowhead); - defs.appendChild(marker); - }); - svg.appendChild(defs); + const LANES = 9; + const ARROWS_PER_LANE = 2; - edges.forEach(([from, to], i) => { - const mx = (from.x + to.x) / 2; - const my = (from.y + to.y) / 2; + edges.forEach(([from, to]) => { const dx = to.x - from.x; const dy = to.y - from.y; const dist = Math.hypot(dx, dy) || 1; - // Bows the edge outward, like a PAGA/CellRank transition graph, instead of a straight line. - const bow = Math.min(dist * 0.22, 60) * (i % 2 === 0 ? 1 : -1); - const cx = mx - (dy / dist) * bow; - const cy = my + (dx / dist) * bow; - - const path = document.createElementNS(svgNS, 'path'); - path.setAttribute('d', `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`); - path.setAttribute('class', 'trajectory-edge'); - path.setAttribute('stroke', to.color); - path.setAttribute('marker-end', `url(#traj-arrow-${i})`); - path.style.animationDelay = `${i * 0.15}s`; - svg.appendChild(path); + const ux = dx / dist; + const uy = dy / dist; + // Perpendicular unit vector: lanes are offset along this so the bundle fans sideways instead of along the direction of travel. + const px = -uy; + const py = ux; + const bow = Math.min(dist * 0.16, 45); + const spread = Math.min(dist * 0.24, 48); + + for (let lane = 0; lane < LANES; lane++) { + // -1 at one edge of the bundle, +1 at the other, 0 down the middle. + const t = LANES === 1 ? 0 : (lane / (LANES - 1)) * 2 - 1; + const jitter = (Math.random() - 0.5) * 6; + const startOffset = t * spread + jitter; + // Streamlines start spread across the source cluster and narrow as they approach the target, like real velocity fields converging on a cell state. + const endOffset = t * spread * 0.2 + jitter; + const midOffset = t * spread * 0.55 + jitter; + + const start = { x: from.x + px * startOffset, y: from.y + py * startOffset }; + const end = { x: to.x + px * endOffset, y: to.y + py * endOffset }; + const cx = (start.x + end.x) / 2 + px * (midOffset + bow); + const cy = (start.y + end.y) / 2 + py * (midOffset + bow); + + const path = document.createElementNS(svgNS, 'path'); + path.setAttribute('d', `M ${start.x} ${start.y} Q ${cx} ${cy} ${end.x} ${end.y}`); + path.setAttribute('class', 'trajectory-line'); + path.setAttribute('stroke', to.color); + svg.appendChild(path); + + const length = path.getTotalLength(); + for (let a = 1; a <= ARROWS_PER_LANE; a++) { + const at = (a / (ARROWS_PER_LANE + 1)) * length; + const p = path.getPointAtLength(at); + const ahead = path.getPointAtLength(Math.min(at + 3, length)); + const angle = (Math.atan2(ahead.y - p.y, ahead.x - p.x) * 180) / Math.PI; + + const arrow = document.createElementNS(svgNS, 'polygon'); + arrow.setAttribute('points', '0,-2.5 5,0 0,2.5'); + arrow.setAttribute('transform', `translate(${p.x}, ${p.y}) rotate(${angle})`); + arrow.setAttribute('class', 'trajectory-arrow'); + arrow.setAttribute('fill', to.color); + svg.appendChild(arrow); + } + } }); - - visualization.appendChild(svg); } function setupDotInteractions() { From bf96b353ee7bd948575fcc849ecabfade26569ff Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:38:16 +0200 Subject: [PATCH 04/21] Color the demo trajectory streamlines black, like a real CellRank/scVelo plot They were tinted per-cluster, which read as decorative rather than data. Real velocity/fate plots draw the flow field in black over the colored cell scatter, so the lines and arrowheads are now black, with more arrows per line for the same dense look. --- assets/main.scss | 8 +++++--- static/main.js | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index 0a71911..792ebbe 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -3109,13 +3109,15 @@ body { .trajectory-line { fill: none; - stroke-width: 1px; + stroke: black; + stroke-width: 0.75px; stroke-linecap: round; - opacity: 0.55; + opacity: 0.65; } .trajectory-arrow { - opacity: 0.75; + fill: black; + opacity: 0.8; } @keyframes trajectory-appear { diff --git a/static/main.js b/static/main.js index b4609e0..b8305b2 100644 --- a/static/main.js +++ b/static/main.js @@ -325,7 +325,7 @@ const initInteractiveViz = () => { colorClusters.forEach(cluster => { const centerX = Math.random() * 0.6 * width + 0.2 * width; const centerY = Math.random() * 0.6 * height + 0.2 * height; - centers.push({ x: centerX, y: centerY, color: cluster.color, name: cluster.name }); + centers.push({ x: centerX, y: centerY }); for (let i = 0; i < cluster.count; i++) { const dot = document.createElement('div'); @@ -397,7 +397,7 @@ const initInteractiveViz = () => { visualization.appendChild(svg); const LANES = 9; - const ARROWS_PER_LANE = 2; + const ARROWS_PER_LANE = 4; edges.forEach(([from, to]) => { const dx = to.x - from.x; @@ -428,7 +428,6 @@ const initInteractiveViz = () => { const path = document.createElementNS(svgNS, 'path'); path.setAttribute('d', `M ${start.x} ${start.y} Q ${cx} ${cy} ${end.x} ${end.y}`); path.setAttribute('class', 'trajectory-line'); - path.setAttribute('stroke', to.color); svg.appendChild(path); const length = path.getTotalLength(); @@ -442,7 +441,6 @@ const initInteractiveViz = () => { arrow.setAttribute('points', '0,-2.5 5,0 0,2.5'); arrow.setAttribute('transform', `translate(${p.x}, ${p.y}) rotate(${angle})`); arrow.setAttribute('class', 'trajectory-arrow'); - arrow.setAttribute('fill', to.color); svg.appendChild(arrow); } } From 3b2f62a987ef9c5561d64fb26b323f4c47b8a3d9 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:42:03 +0200 Subject: [PATCH 05/21] Make the demo clusters denser and elongated instead of sparse round scatters Real UMAP clusters are dense, overlapping, tapered blobs, not evenly spread circles. Roughly tripled the point count per cluster and replaced the isotropic Gaussian placement with an elongated axis per cluster (with a narrower cross-axis), so clusters read as solid shapes closer to the CellRank/scVelo reference instead of a loose scatter. --- static/main.js | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/static/main.js b/static/main.js index b8305b2..a1409f1 100644 --- a/static/main.js +++ b/static/main.js @@ -269,14 +269,14 @@ const initInteractiveViz = () => { // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ - { color: '#40a9ff', count: 34, name: 'Cluster A' }, - { color: '#4ab274', count: 29, name: 'Cluster B' }, - { color: '#fbb822', count: 26, name: 'Cluster C' }, - { color: '#e5864b', count: 22, name: 'Cluster D' }, - { color: '#da347f', count: 27, name: 'Cluster E' }, - { color: '#969dea', count: 24, name: 'Cluster F' }, - { color: '#de367b', count: 19, name: 'Cluster G' }, - { color: '#6cf1a1', count: 31, name: 'Cluster H' } + { color: '#40a9ff', count: 90, name: 'Cluster A' }, + { color: '#4ab274', count: 78, name: 'Cluster B' }, + { color: '#fbb822', count: 70, name: 'Cluster C' }, + { color: '#e5864b', count: 58, name: 'Cluster D' }, + { color: '#da347f', count: 72, name: 'Cluster E' }, + { color: '#969dea', count: 64, name: 'Cluster F' }, + { color: '#de367b', count: 50, name: 'Cluster G' }, + { color: '#6cf1a1', count: 82, name: 'Cluster H' } ]; // 3D tilt effect @@ -327,13 +327,22 @@ const initInteractiveViz = () => { const centerY = Math.random() * 0.6 * height + 0.2 * height; centers.push({ x: centerX, y: centerY }); + // Real UMAP clusters are elongated, tapered blobs rather than round scatters, so each cluster gets its own axis to stretch along and a perpendicular axis to stay narrow on. + const axisAngle = Math.random() * Math.PI * 2; + const axisX = Math.cos(axisAngle); + const axisY = Math.sin(axisAngle); + const perpX = -axisY; + const perpY = axisX; + const axisLength = Math.random() * 40 + 55; + const perpWidth = axisLength * (Math.random() * 0.2 + 0.28); + for (let i = 0; i < cluster.count; i++) { const dot = document.createElement('div'); dot.className = 'dot'; dot.dataset.cluster = cluster.name; dot.dataset.color = cluster.color; - const size = Math.floor(Math.random() * 8) + 5; + const size = Math.floor(Math.random() * 5) + 4; dot.style.width = `${size}px`; dot.style.height = `${size}px`; @@ -345,11 +354,10 @@ const initInteractiveViz = () => { u = u / 6 - 0.5; v = v / 6 - 0.5; - const distance = Math.random() * 130 + 20; - const dx = u * distance * 2; - const dy = v * distance * 2; - const x = centerX + dx; - const y = centerY + dy; + const along = u * axisLength; + const across = v * perpWidth; + const x = centerX + axisX * along + perpX * across; + const y = centerY + axisY * along + perpY * across; const safeX = Math.min(Math.max(size, x), width - size); const safeY = Math.min(Math.max(size, y), height - size); @@ -366,12 +374,12 @@ const initInteractiveViz = () => { setTimeout(() => { dot.style.transform = 'scale(1)'; dot.style.opacity = '1'; - }, i * 18 + Math.random() * 150); + }, i * 8 + Math.random() * 150); } }); setupDotInteractions(); - setTimeout(() => drawTrajectory(centers), 850); + setTimeout(() => drawTrajectory(centers), 950); } // Draws a scVelo/CellRank-style velocity field: bundles of parallel arrowed streamlines that fan out from a cluster and converge into the next one, rather than one graph edge between cluster centroids. @@ -408,8 +416,8 @@ const initInteractiveViz = () => { // Perpendicular unit vector: lanes are offset along this so the bundle fans sideways instead of along the direction of travel. const px = -uy; const py = ux; - const bow = Math.min(dist * 0.16, 45); - const spread = Math.min(dist * 0.24, 48); + const bow = Math.min(dist * 0.16, 36); + const spread = Math.min(dist * 0.22, 36); for (let lane = 0; lane < LANES; lane++) { // -1 at one edge of the bundle, +1 at the other, 0 down the middle. From ead563bb4c61e77f7b2f65c35bdb92f6bfb894a0 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:46:37 +0200 Subject: [PATCH 06/21] Lay demo cluster centers out along a connected chain instead of scattering them randomly Clusters were placed independently across the box, so most pairs ended up far apart relative to their own size, with long thin streamline bridges between distant islands. Centers are now placed along a winding, rightward-biased walk (with the last two branching off the end), so neighbouring clusters touch and the whole thing reads as one connected embedding, closer to a real CellRank plot. Bumped counts and blob size to match the larger spacing. --- static/main.js | 60 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/static/main.js b/static/main.js index a1409f1..a2a8650 100644 --- a/static/main.js +++ b/static/main.js @@ -269,14 +269,14 @@ const initInteractiveViz = () => { // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ - { color: '#40a9ff', count: 90, name: 'Cluster A' }, - { color: '#4ab274', count: 78, name: 'Cluster B' }, - { color: '#fbb822', count: 70, name: 'Cluster C' }, - { color: '#e5864b', count: 58, name: 'Cluster D' }, - { color: '#da347f', count: 72, name: 'Cluster E' }, - { color: '#969dea', count: 64, name: 'Cluster F' }, - { color: '#de367b', count: 50, name: 'Cluster G' }, - { color: '#6cf1a1', count: 82, name: 'Cluster H' } + { color: '#40a9ff', count: 130, name: 'Cluster A' }, + { color: '#4ab274', count: 112, name: 'Cluster B' }, + { color: '#fbb822', count: 100, name: 'Cluster C' }, + { color: '#e5864b', count: 84, name: 'Cluster D' }, + { color: '#da347f', count: 104, name: 'Cluster E' }, + { color: '#969dea', count: 92, name: 'Cluster F' }, + { color: '#de367b', count: 72, name: 'Cluster G' }, + { color: '#6cf1a1', count: 118, name: 'Cluster H' } ]; // 3D tilt effect @@ -320,12 +320,33 @@ const initInteractiveViz = () => { const width = visualization.clientWidth; const height = visualization.clientHeight; + + // Lays cluster centers out along a winding chain instead of scattering them independently, so neighbouring clusters sit close enough to touch, like a real connected UMAP embedding, and the last two branch off from the chain's end. const centers = []; + // Biased toward rightward travel (angle 0) so the chain fans out across this wide, short box instead of a random walk drifting into a corner; it can still wiggle up and down along the way. + let angle = (Math.random() - 0.5) * 0.6; + let cx = width * (0.05 + Math.random() * 0.05); + let cy = height * (0.35 + Math.random() * 0.3); + const chainCount = colorClusters.length - 2; + for (let i = 0; i < chainCount; i++) { + if (i > 0) { + angle = angle * 0.6 + (Math.random() - 0.5) * 1.1; + const step = 110 + Math.random() * 40; + cx = Math.min(Math.max(cx + Math.cos(angle) * step, width * 0.04), width * 0.96); + cy = Math.min(Math.max(cy + Math.sin(angle) * step, height * 0.12), height * 0.88); + } + centers.push({ x: cx, y: cy }); + } + [angle + 0.7 + Math.random() * 0.3, angle - 0.7 - Math.random() * 0.3].forEach(branchAngle => { + const step = 105 + Math.random() * 40; + centers.push({ + x: Math.min(Math.max(cx + Math.cos(branchAngle) * step, width * 0.03), width * 0.97), + y: Math.min(Math.max(cy + Math.sin(branchAngle) * step, height * 0.05), height * 0.95) + }); + }); - colorClusters.forEach(cluster => { - const centerX = Math.random() * 0.6 * width + 0.2 * width; - const centerY = Math.random() * 0.6 * height + 0.2 * height; - centers.push({ x: centerX, y: centerY }); + colorClusters.forEach((cluster, clusterIndex) => { + const { x: centerX, y: centerY } = centers[clusterIndex]; // Real UMAP clusters are elongated, tapered blobs rather than round scatters, so each cluster gets its own axis to stretch along and a perpendicular axis to stay narrow on. const axisAngle = Math.random() * Math.PI * 2; @@ -333,7 +354,7 @@ const initInteractiveViz = () => { const axisY = Math.sin(axisAngle); const perpX = -axisY; const perpY = axisX; - const axisLength = Math.random() * 40 + 55; + const axisLength = Math.random() * 55 + 95; const perpWidth = axisLength * (Math.random() * 0.2 + 0.28); for (let i = 0; i < cluster.count; i++) { @@ -389,14 +410,15 @@ const initInteractiveViz = () => { if (old) old.remove(); if (centers.length < 4) return; - const ordered = [...centers].sort((a, b) => a.x - b.x); + // centers is already laid out as a chain with the last two entries branching off its end, matching how generateUMAP placed the cluster centers, so the edges just follow that same order. + const chainCount = centers.length - 2; const edges = []; - for (let i = 0; i < ordered.length - 3; i++) { - edges.push([ordered[i], ordered[i + 1]]); + for (let i = 0; i < chainCount - 1; i++) { + edges.push([centers[i], centers[i + 1]]); } - const branchFrom = ordered[ordered.length - 3]; - edges.push([branchFrom, ordered[ordered.length - 2]]); - edges.push([branchFrom, ordered[ordered.length - 1]]); + const branchFrom = centers[chainCount - 1]; + edges.push([branchFrom, centers[chainCount]]); + edges.push([branchFrom, centers[chainCount + 1]]); const svg = document.createElementNS(svgNS, 'svg'); svg.setAttribute('class', 'trajectory-svg'); From af8a5b6e250b12258f600d036830ca9986fb1742 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:52:12 +0200 Subject: [PATCH 07/21] Roughly double the demo point count and overlap adjacent clusters more Clusters were touching but not overlapping, and still looked thin compared to real scRNA-seq embeddings where neighbouring clusters often blend into each other. Point counts per cluster are up significantly, and the spacing between chain steps is now smaller than the cluster blob radius, so neighbours visibly overlap instead of just meeting at an edge. --- static/main.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/static/main.js b/static/main.js index a2a8650..47da500 100644 --- a/static/main.js +++ b/static/main.js @@ -269,14 +269,14 @@ const initInteractiveViz = () => { // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ - { color: '#40a9ff', count: 130, name: 'Cluster A' }, - { color: '#4ab274', count: 112, name: 'Cluster B' }, - { color: '#fbb822', count: 100, name: 'Cluster C' }, - { color: '#e5864b', count: 84, name: 'Cluster D' }, - { color: '#da347f', count: 104, name: 'Cluster E' }, - { color: '#969dea', count: 92, name: 'Cluster F' }, - { color: '#de367b', count: 72, name: 'Cluster G' }, - { color: '#6cf1a1', count: 118, name: 'Cluster H' } + { color: '#40a9ff', count: 240, name: 'Cluster A' }, + { color: '#4ab274', count: 205, name: 'Cluster B' }, + { color: '#fbb822', count: 185, name: 'Cluster C' }, + { color: '#e5864b', count: 155, name: 'Cluster D' }, + { color: '#da347f', count: 190, name: 'Cluster E' }, + { color: '#969dea', count: 170, name: 'Cluster F' }, + { color: '#de367b', count: 135, name: 'Cluster G' }, + { color: '#6cf1a1', count: 215, name: 'Cluster H' } ]; // 3D tilt effect @@ -331,14 +331,14 @@ const initInteractiveViz = () => { for (let i = 0; i < chainCount; i++) { if (i > 0) { angle = angle * 0.6 + (Math.random() - 0.5) * 1.1; - const step = 110 + Math.random() * 40; + const step = 85 + Math.random() * 30; cx = Math.min(Math.max(cx + Math.cos(angle) * step, width * 0.04), width * 0.96); cy = Math.min(Math.max(cy + Math.sin(angle) * step, height * 0.12), height * 0.88); } centers.push({ x: cx, y: cy }); } [angle + 0.7 + Math.random() * 0.3, angle - 0.7 - Math.random() * 0.3].forEach(branchAngle => { - const step = 105 + Math.random() * 40; + const step = 70 + Math.random() * 25; centers.push({ x: Math.min(Math.max(cx + Math.cos(branchAngle) * step, width * 0.03), width * 0.97), y: Math.min(Math.max(cy + Math.sin(branchAngle) * step, height * 0.05), height * 0.95) @@ -354,7 +354,7 @@ const initInteractiveViz = () => { const axisY = Math.sin(axisAngle); const perpX = -axisY; const perpY = axisX; - const axisLength = Math.random() * 55 + 95; + const axisLength = Math.random() * 65 + 110; const perpWidth = axisLength * (Math.random() * 0.2 + 0.28); for (let i = 0; i < cluster.count; i++) { @@ -395,7 +395,7 @@ const initInteractiveViz = () => { setTimeout(() => { dot.style.transform = 'scale(1)'; dot.style.opacity = '1'; - }, i * 8 + Math.random() * 150); + }, i * 3 + Math.random() * 150); } }); From d1f2c093556be857ab4c6bfbf80512e42b99f880 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:54:45 +0200 Subject: [PATCH 08/21] Center the demo trajectory and double the point count again The biased random walk building the cluster chain tended to drift toward one side of the box, leaving the middle empty and the shape crammed into a corner. The chain's bounding box is now re-centered on the container after layout. Also doubled point counts again (now ~3,000 total) for a denser, more solid-looking scatter. --- static/main.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/static/main.js b/static/main.js index 47da500..d627542 100644 --- a/static/main.js +++ b/static/main.js @@ -269,14 +269,14 @@ const initInteractiveViz = () => { // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ - { color: '#40a9ff', count: 240, name: 'Cluster A' }, - { color: '#4ab274', count: 205, name: 'Cluster B' }, - { color: '#fbb822', count: 185, name: 'Cluster C' }, - { color: '#e5864b', count: 155, name: 'Cluster D' }, - { color: '#da347f', count: 190, name: 'Cluster E' }, - { color: '#969dea', count: 170, name: 'Cluster F' }, - { color: '#de367b', count: 135, name: 'Cluster G' }, - { color: '#6cf1a1', count: 215, name: 'Cluster H' } + { color: '#40a9ff', count: 480, name: 'Cluster A' }, + { color: '#4ab274', count: 410, name: 'Cluster B' }, + { color: '#fbb822', count: 370, name: 'Cluster C' }, + { color: '#e5864b', count: 310, name: 'Cluster D' }, + { color: '#da347f', count: 380, name: 'Cluster E' }, + { color: '#969dea', count: 340, name: 'Cluster F' }, + { color: '#de367b', count: 270, name: 'Cluster G' }, + { color: '#6cf1a1', count: 430, name: 'Cluster H' } ]; // 3D tilt effect @@ -345,6 +345,16 @@ const initInteractiveViz = () => { }); }); + // The walk above tends to drift toward one side, so re-center its bounding box on the container instead of leaving the whole shape off to one edge. + const xs = centers.map(c => c.x); + const ys = centers.map(c => c.y); + const shiftX = width / 2 - (Math.min(...xs) + Math.max(...xs)) / 2; + const shiftY = height / 2 - (Math.min(...ys) + Math.max(...ys)) / 2; + centers.forEach(c => { + c.x += shiftX; + c.y += shiftY; + }); + colorClusters.forEach((cluster, clusterIndex) => { const { x: centerX, y: centerY } = centers[clusterIndex]; From 36dbda783838c2c59cf1cb6a089d24ccfb94cbe6 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 11:57:37 +0200 Subject: [PATCH 09/21] Double the demo point count again and grow the cluster blobs to fill the box Point counts per cluster are doubled again (~6,000 total). Also grew the cluster blob size and per-dot size, since the box still had a lot of empty space with clusters that stayed small even as they got denser. --- static/main.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/static/main.js b/static/main.js index d627542..ecf4b3e 100644 --- a/static/main.js +++ b/static/main.js @@ -269,14 +269,14 @@ const initInteractiveViz = () => { // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ - { color: '#40a9ff', count: 480, name: 'Cluster A' }, - { color: '#4ab274', count: 410, name: 'Cluster B' }, - { color: '#fbb822', count: 370, name: 'Cluster C' }, - { color: '#e5864b', count: 310, name: 'Cluster D' }, - { color: '#da347f', count: 380, name: 'Cluster E' }, - { color: '#969dea', count: 340, name: 'Cluster F' }, - { color: '#de367b', count: 270, name: 'Cluster G' }, - { color: '#6cf1a1', count: 430, name: 'Cluster H' } + { color: '#40a9ff', count: 960, name: 'Cluster A' }, + { color: '#4ab274', count: 820, name: 'Cluster B' }, + { color: '#fbb822', count: 740, name: 'Cluster C' }, + { color: '#e5864b', count: 620, name: 'Cluster D' }, + { color: '#da347f', count: 760, name: 'Cluster E' }, + { color: '#969dea', count: 680, name: 'Cluster F' }, + { color: '#de367b', count: 540, name: 'Cluster G' }, + { color: '#6cf1a1', count: 860, name: 'Cluster H' } ]; // 3D tilt effect @@ -364,8 +364,8 @@ const initInteractiveViz = () => { const axisY = Math.sin(axisAngle); const perpX = -axisY; const perpY = axisX; - const axisLength = Math.random() * 65 + 110; - const perpWidth = axisLength * (Math.random() * 0.2 + 0.28); + const axisLength = Math.random() * 90 + 150; + const perpWidth = axisLength * (Math.random() * 0.25 + 0.4); for (let i = 0; i < cluster.count; i++) { const dot = document.createElement('div'); @@ -373,7 +373,7 @@ const initInteractiveViz = () => { dot.dataset.cluster = cluster.name; dot.dataset.color = cluster.color; - const size = Math.floor(Math.random() * 5) + 4; + const size = Math.floor(Math.random() * 6) + 5; dot.style.width = `${size}px`; dot.style.height = `${size}px`; From 6cfef9443136e7cd2bf23e863a66b77e9e19d75f Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 12:09:02 +0200 Subject: [PATCH 10/21] Add a third demo command for the velocity streamlines The trajectory overlay was tacked onto sc.pl.umap(), but a UMAP call doesn't produce a velocity field. Added a third fake command line, scv.pl.velocity_embedding_stream(data, basis='umap'), matching the real scVelo function that plots this. sc.pl.umap() now only (re)plots the scatter; the streamlines are drawn by the new command, independently clickable, and still auto-play once on load and after resize so a visitor who never clicks anything still sees all three steps. --- layouts/index.html | 18 ++++++++++++++++++ static/main.js | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/layouts/index.html b/layouts/index.html index a04c539..2ff374d 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -56,6 +56,24 @@
+
+ +
scv.pl.velocity_embedding_stream(data, basis='umap')
+
+
+
diff --git a/static/main.js b/static/main.js index ecf4b3e..e10c6e0 100644 --- a/static/main.js +++ b/static/main.js @@ -259,14 +259,20 @@ const initInteractiveViz = () => { const visualization = document.getElementById('visualization'); const runCmd1 = document.getElementById('run-cmd1'); const runCmd2 = document.getElementById('run-cmd2'); + const runCmd3 = document.getElementById('run-cmd3'); const statusCmd1 = document.getElementById('status-cmd1'); const statusCmd2 = document.getElementById('status-cmd2'); + const statusCmd3 = document.getElementById('status-cmd3'); const execAnim1 = document.getElementById('exec-anim-1'); const execAnim2 = document.getElementById('exec-anim-2'); + const execAnim3 = document.getElementById('exec-anim-3'); // Exit early if visualization elements don't exist on this page if (!visualization || !card) return; + // Cluster centers from the most recent sc.pl.umap() run, so scv.pl.velocity_embedding_stream() can draw its streamlines over the same layout without recomputing it. + let lastCenters = null; + // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. const colorClusters = [ { color: '#40a9ff', count: 960, name: 'Cluster A' }, @@ -410,7 +416,7 @@ const initInteractiveViz = () => { }); setupDotInteractions(); - setTimeout(() => drawTrajectory(centers), 950); + lastCenters = centers; } // Draws a scVelo/CellRank-style velocity field: bundles of parallel arrowed streamlines that fan out from a cluster and converge into the next one, rather than one graph edge between cluster centroids. @@ -536,7 +542,7 @@ const initInteractiveViz = () => { } // These are divs, not buttons, so the keyboard behaviour has to be added by hand. - [runCmd1, runCmd2].forEach(function(control) { + [runCmd1, runCmd2, runCmd3].forEach(function(control) { control.addEventListener('keydown', function(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); @@ -588,14 +594,38 @@ const initInteractiveViz = () => { }, 1200); }); - // Initial generation + runCmd3.addEventListener('click', function() { + statusCmd3.style.width = '0'; + execAnim3.style.width = '0'; + + setTimeout(() => { + execAnim3.style.width = '100%'; + }, 50); + + setTimeout(() => { + if (lastCenters) drawTrajectory(lastCenters); + + statusCmd3.style.width = '100%'; + runCmd3.style.backgroundColor = '#34A853'; + + setTimeout(() => { + runCmd3.style.backgroundColor = ''; + }, 2000); + }, 900); + }); + + // Initial generation, followed by the velocity streamlines once the scatter has settled, so a visitor who never clicks anything still sees the full three-command story play out once. generateUMAP(); + setTimeout(() => runCmd3.click(), 1200); // Responsive regeneration let resizeTimer; window.addEventListener('resize', function() { clearTimeout(resizeTimer); - resizeTimer = setTimeout(generateUMAP, 250); + resizeTimer = setTimeout(function() { + generateUMAP(); + setTimeout(() => runCmd3.click(), 1200); + }, 250); }); } From d9195879afab588a49466bc337b814759df5a140 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 12:34:17 +0200 Subject: [PATCH 11/21] Make the third demo command generic instead of naming scVelo scv.pl.velocity_embedding_stream() calls out a specific ecosystem package that isn't part of scverse core, which reads oddly on the homepage hero. Renamed to the package-agnostic pl.trajectory(), keeping the same three-step demo structure without endorsing one tool. --- layouts/index.html | 4 ++-- static/main.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/layouts/index.html b/layouts/index.html index 2ff374d..3242267 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -62,7 +62,7 @@ id="run-cmd3" type="button" tabindex="0" - aria-label="Run scv.pl.velocity_embedding_stream(data, basis='umap')" + aria-label="Run pl.trajectory(data, basis='umap')" > -
scv.pl.velocity_embedding_stream(data, basis='umap')
+
pl.trajectory(data, basis='umap')
diff --git a/static/main.js b/static/main.js index e10c6e0..5d324fc 100644 --- a/static/main.js +++ b/static/main.js @@ -270,7 +270,7 @@ const initInteractiveViz = () => { // Exit early if visualization elements don't exist on this page if (!visualization || !card) return; - // Cluster centers from the most recent sc.pl.umap() run, so scv.pl.velocity_embedding_stream() can draw its streamlines over the same layout without recomputing it. + // Cluster centers from the most recent sc.pl.umap() run, so pl.trajectory() can draw its streamlines over the same layout without recomputing it. let lastCenters = null; // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. From 3ce82269bb2da70d3bbe7e116ce2b44ca03232be Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 13:55:10 +0200 Subject: [PATCH 12/21] Explain what scverse is in the hero, expand the highlight grid to 6 cards, and clean up the hero chrome The hero previously only had a one-line tagline with no explanation of what scverse actually is or who it's for. Added an nf-core-style explanatory paragraph plus three audience value props (researchers, developers, institutions) right under the title. Added Core packages and Events cards to the highlight grid, growing it from a 2x2 to an auto-fitting 3-column grid (2 rows of 3 on desktop). Removed the row of social platform buttons from the hero: they duplicate the same links already in the footer and were adding clutter with no unique content. Expanded the conference announcement chip with the event's location and reduced its margin, so it carries more information without floating in as much empty space. --- assets/main.scss | 55 ++++++++++++++++++++------------ content/_index.md | 29 +++++++++++++++++ layouts/index.html | 22 +++++++++++++ layouts/partials/main/chip.html | 7 ++-- layouts/partials/main/cover.html | 27 +++++++--------- 5 files changed, 101 insertions(+), 39 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index 792ebbe..fdc8d23 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -135,12 +135,6 @@ $eco-hues: } } -// Cover buttons -#badges .btn { - background-color: $tilebg; - border: none; -} - #join-content .card { h5 { font-weight: 700; @@ -153,32 +147,26 @@ $eco-hues: } } -#badges .btn-github:hover, #join-content .card#github:hover { background-color: $github-black !important; color: white !important; } -#badges .btn-discourse:hover, #join-content .card#discourse:hover { background-color: $discourse-yellow !important; color: black !important; } -#badges .btn-zulip:hover, #join-content .card#zulip:hover { background-color: $zulip-blue !important; color: white !important; } -#badges .btn-twitter:hover, #join-content .card#twitter:hover { background-color: $twitter-blue !important; color: white !important; } -#badges .btn-bluesky:hover, #join-content .card#bluesky:hover { background-color: $bluesky-blue !important; color: white !important; } -#badges .btn-youtube:hover, #join-content .card#youtube:hover { background-color: $youtube-red !important; color: white !important; @@ -385,21 +373,46 @@ body { } } } - #badges { + + #hero-lede { + max-width: 42rem; + margin: 1.5rem 0 0; + font-size: 1.1rem; + line-height: 1.6; + color: $tiletext2; + @media (max-width: 50rem) { + font-size: 0.95rem; + } + } + + #hero-points { display: flex; flex-wrap: wrap; - gap: 1rem; - margin: 2rem 0; + gap: 2rem; + margin: 2rem 0 0; @media (max-width: 50rem) { - margin: 1rem 0; - padding: 0; - width: 100%; + gap: 1.25rem; + margin-top: 1.5rem; + } + .hero-point { + flex: 1 1 14rem; + max-width: 18rem; + } + .hero-point-title { + font-weight: 700; + color: $tiletext; + margin-bottom: 0.3rem; + } + .hero-point-text { + margin: 0; + font-size: 0.9rem; + color: $tiletext2; } } } #chip-announcement { - margin: 0 auto 2rem; + margin: 0 auto 1.5rem; max-width: $maxwidthcover; width: 100%; @media (max-width: 50rem) { @@ -592,11 +605,11 @@ body { #about-highlights { .about-card-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + // auto-fit rather than a fixed column count: the container is capped around 960px, so this settles at 3 columns on desktop and folds down on its own as the viewport narrows, without a hand-tuned breakpoint. + grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 1.5rem; @media (max-width: 50rem) { - grid-template-columns: 1fr; gap: 1rem; } } diff --git a/content/_index.md b/content/_index.md index 996bc1e..0ea04d3 100644 --- a/content/_index.md +++ b/content/_index.md @@ -2,6 +2,35 @@ title = "scverse" description = "Foundational tools for single-cell omics data analysis" +# Hero +[hero] + +text = """\ +scverse is an open-source, community-governed ecosystem of interoperable Python packages for single-cell and spatial omics data analysis — from foundational data structures like AnnData to analysis frameworks like Scanpy, scvi-tools, and Squidpy. The tools share common data formats, so you can move between them in a single analysis instead of gluing together incompatible pipelines.""" + +[[hero.points]] +title = "For researchers" +text = "Well-documented, actively maintained tools that interoperate out of the box." + +[[hero.points]] +title = "For developers" +text = "Shared data structures and conventions, so your package fits the ecosystem from day one." + +[[hero.points]] +title = "For institutions" +text = "A community-governed foundation ensuring the long-term maintenance of critical infrastructure." + +# Core packages +[packages] + +text = """\ +scverse maintains a set of interoperable core packages spanning every step of single-cell and spatial omics analysis, from data structures to modeling.""" + +# Events +[events] + +text = """\ +scverse hosts hackathons, workshops, and an annual conference to bring the community together throughout the year.""" # Mission [mission] diff --git a/layouts/index.html b/layouts/index.html index 3242267..1c5edbb 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -110,6 +110,17 @@

{{ .Title }}

+
+
+

Core packages

+

+ {{ .Params.packages.text | markdownify }} +

+ +
Browse the core packages
+
+
+

Ecosystem

@@ -121,6 +132,17 @@

Ecosystem

+
+
+

Events

+

+ {{ .Params.events.text | markdownify }} +

+ +
See all scverse events
+
+
+

Mission

diff --git a/layouts/partials/main/chip.html b/layouts/partials/main/chip.html index 947bbf7..db2b35f 100644 --- a/layouts/partials/main/chip.html +++ b/layouts/partials/main/chip.html @@ -1,7 +1,10 @@
-
October 12 – 14
-
scverse conference 2026: Register now!
+
Oct 12–14, 2026
+
+ scverse conference 2026 · Copenhagen, Denmark — registration is open + +
diff --git a/layouts/partials/main/cover.html b/layouts/partials/main/cover.html index d18900c..40b7635 100644 --- a/layouts/partials/main/cover.html +++ b/layouts/partials/main/cover.html @@ -5,20 +5,15 @@

{{ .Title }}

{{ .Description }}
- + {{ with .Params.hero }} +

{{ .text | markdownify }}

+
+ {{ range .points }} +
+
{{ .title }}
+

{{ .text }}

+
+ {{ end }} +
+ {{ end }} From a9a08ece67eb49ab9ff43c17d6d158bfae0d3760 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 14:06:12 +0200 Subject: [PATCH 13/21] Widen the homepage layout, remove now-redundant sections, and fix the trajectory command name The homepage capped content at 1200px/80% while other pages already had a "wide" variant (1400px/92%); switched the hero, chip, and section widths to that wide variant so the page uses more of a large monitor instead of leaving big unused margins on either side. Removed the "Next event" article and the full core-packages tile grid from the homepage now that the highlight grid has its own Core packages and Events cards linking to the same destinations. Deleted the now-unused main/packages.html partial and its dead CSS. Pinned the highlight grid to a fixed 3-column layout instead of auto-fit, which was squeezing in a 4th column on wide viewports and breaking the intended 3+3 layout of the 6 cards. Renamed the third demo command from pl.trajectory(...) to sc.pl.trajectory(...): the bare pl. prefix didn't correspond to anything import scanpy as sc actually imports. --- assets/main.scss | 86 +++++------------------------ layouts/index.html | 31 +---------- layouts/partials/main/packages.html | 38 ------------- static/main.js | 2 +- 4 files changed, 16 insertions(+), 141 deletions(-) delete mode 100644 layouts/partials/main/packages.html diff --git a/assets/main.scss b/assets/main.scss index fdc8d23..a5d5543 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -46,7 +46,6 @@ $footertextmuted: #aaaaaa; $postlinktext: #333333; $maxwidth: 1200px; -$maxwidthcover: 1200px; $maxwidthwide: 1400px; $contentwidth: 80%; $contentwidthwide: 92%; @@ -339,7 +338,7 @@ body { align-items: center; align-self: center; #cover { - max-width: $maxwidthcover; + max-width: $maxwidthwide; width: 100%; padding: 0 1rem; #cover-top-section { @@ -349,7 +348,7 @@ body { #cover-heading { margin: 0; padding: 0; - max-width: $maxwidthcover; + max-width: $maxwidthwide; #title { #title-text { #title-name { @@ -413,7 +412,7 @@ body { #chip-announcement { margin: 0 auto 1.5rem; - max-width: $maxwidthcover; + max-width: $maxwidthwide; width: 100%; @media (max-width: 50rem) { width: 100%; @@ -505,8 +504,8 @@ body { } section { - max-width: $maxwidth; - width: $contentwidth; + max-width: $maxwidthwide; + width: $contentwidthwide; margin: 0 auto; margin-top: 5%; @media (max-width: 50rem) { @@ -538,78 +537,19 @@ body { #interactive-demo-section { margin-bottom: 3rem; } - #packages { - #packages-list { - .card { - display: flex; - flex-direction: row; - align-items: center; - background-color: $tilebg; - border-radius: 0.5rem; - transition: all 200ms ease-in-out; - @media (max-width: 50rem) { - min-height: 6rem; - } - .package-icon { - transition: all 200ms ease-in-out; - align-items: center; - justify-content: center; - padding: auto; - display: flex; - min-width: 6rem; - max-width: 6rem; - // Matches the img height below, so that a package - // without an icon still lines up with the others. - min-height: 4rem; - @media (max-width: 50rem) { - display: none; - } - - img { - max-width: 4rem; - height: 4rem; - } - } - .package-text { - .package-name { - color: $tiletext; - } - .package-desc { - font-size: 0.9rem; - color: $tiletext2; - } - } - &:hover { - cursor: pointer; - background-color: $tilebg2; - .package-icon { - transform: scale(1.1); - } - transform: scale(1.05); - } - &#anndata-tile:hover { - background-color: rgba($anndataorange, 0.2); - } - &#scanpy-tile:hover { - background-color: rgba($scanpymandy, 0.2); - } - &#mudata-tile:hover { - background-color: rgba($mudatagreen, 0.2); - } - &#muon-tile:hover { - background-color: rgba($muonaquamarine, 0.2); - } - } - } - } #about-highlights { .about-card-grid { display: grid; - // auto-fit rather than a fixed column count: the container is capped around 960px, so this settles at 3 columns on desktop and folds down on its own as the viewport narrows, without a hand-tuned breakpoint. - grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); + // A fixed 3-column count rather than auto-fit: on a wide viewport auto-fit would squeeze in a 4th column and break the 3+3 layout of 6 cards. + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem; + @media (max-width: 64rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + @media (max-width: 50rem) { + grid-template-columns: 1fr; gap: 1rem; } } @@ -2984,7 +2924,7 @@ body { .interactive-container { width: 100%; - max-width: 1200px; + max-width: $maxwidthwide; margin: 0 auto; transform-style: preserve-3d; perspective: 1500px; diff --git a/layouts/index.html b/layouts/index.html index 1c5edbb..5fca686 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -62,7 +62,7 @@ id="run-cmd3" type="button" tabindex="0" - aria-label="Run pl.trajectory(data, basis='umap')" + aria-label="Run sc.pl.trajectory(data, basis='umap')" > -
pl.trajectory(data, basis='umap')
+
sc.pl.trajectory(data, basis='umap')
@@ -81,33 +81,6 @@
- {{ with first 1 (where (where .Site.RegularPages "Section" "events") "Date.Unix" ">" now.Unix).ByDate }} -
-
Next event
- -
- {{ end }} - - {{ partial "main/packages" . }} - -
diff --git a/layouts/partials/main/packages.html b/layouts/partials/main/packages.html deleted file mode 100644 index fc1794f..0000000 --- a/layouts/partials/main/packages.html +++ /dev/null @@ -1,38 +0,0 @@ -
-
Core packages
-
- {{ $registry := partialCached "registry.html" "registry" }} - {{ with $registry.core }} -
- {{ range $p := . }} -
-
-
- {{ with .logo }} - Logo for {{ $p.name }} - {{ end }} -
-
- -
- {{ .name }} -
-

- {{ .blurb }} -

-
-
-
- {{ end }} -
- {{ end }} - -
View all scverse packages
-
-
-
diff --git a/static/main.js b/static/main.js index 5d324fc..1fc6bbc 100644 --- a/static/main.js +++ b/static/main.js @@ -270,7 +270,7 @@ const initInteractiveViz = () => { // Exit early if visualization elements don't exist on this page if (!visualization || !card) return; - // Cluster centers from the most recent sc.pl.umap() run, so pl.trajectory() can draw its streamlines over the same layout without recomputing it. + // Cluster centers from the most recent sc.pl.umap() run, so sc.pl.trajectory() can draw its streamlines over the same layout without recomputing it. let lastCenters = null; // Color clusters for UMAP visualization, using the same brand hues the package tiles below use instead of a generic chart-library palette. From d76aae67d7a1557a94d1c181ca34343bae359afc Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 14:13:02 +0200 Subject: [PATCH 14/21] Widen the hero lede, add icons to the audience value props The lede paragraph was capped at 42rem and the value props at 18rem each, so both looked stranded in a narrow column on the left of the now-wide hero with a lot of unused space beside them, and the paragraph wrapped into more lines than it needed to. Widened both to 58rem, and gave each "For X" value prop an icon to match the visual weight of the rest of the page instead of being bare text. --- assets/main.scss | 16 ++++++++++++++-- content/_index.md | 3 +++ layouts/partials/main/cover.html | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index a5d5543..a9bcad8 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -374,7 +374,7 @@ body { } #hero-lede { - max-width: 42rem; + max-width: 58rem; margin: 1.5rem 0 0; font-size: 1.1rem; line-height: 1.6; @@ -388,6 +388,7 @@ body { display: flex; flex-wrap: wrap; gap: 2rem; + max-width: 58rem; margin: 2rem 0 0; @media (max-width: 50rem) { gap: 1.25rem; @@ -395,7 +396,18 @@ body { } .hero-point { flex: 1 1 14rem; - max-width: 18rem; + } + .hero-point-icon { + width: 2.25rem; + height: 2.25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + margin-bottom: 0.6rem; + font-size: 1.1rem; + background-color: $tilebg; + color: $tiletext; } .hero-point-title { font-weight: 700; diff --git a/content/_index.md b/content/_index.md index 0ea04d3..63815fb 100644 --- a/content/_index.md +++ b/content/_index.md @@ -9,14 +9,17 @@ text = """\ scverse is an open-source, community-governed ecosystem of interoperable Python packages for single-cell and spatial omics data analysis — from foundational data structures like AnnData to analysis frameworks like Scanpy, scvi-tools, and Squidpy. The tools share common data formats, so you can move between them in a single analysis instead of gluing together incompatible pipelines.""" [[hero.points]] +icon = "bi-mortarboard" title = "For researchers" text = "Well-documented, actively maintained tools that interoperate out of the box." [[hero.points]] +icon = "bi-code-slash" title = "For developers" text = "Shared data structures and conventions, so your package fits the ecosystem from day one." [[hero.points]] +icon = "bi-bank" title = "For institutions" text = "A community-governed foundation ensuring the long-term maintenance of critical infrastructure." diff --git a/layouts/partials/main/cover.html b/layouts/partials/main/cover.html index 40b7635..cd0b256 100644 --- a/layouts/partials/main/cover.html +++ b/layouts/partials/main/cover.html @@ -10,6 +10,7 @@

{{ .Title }}

{{ range .points }}
+
{{ .title }}

{{ .text }}

From 26f03990b2a1c4f340b68d87e5855f0f9ced97e2 Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 14:19:45 +0200 Subject: [PATCH 15/21] Center the hero, redesign the conference banner as a bold card, and add real hero CTAs Centered the whole hero block (title, tagline, lede, value props) instead of leaving it left-aligned while the rest of the page reads as centered. Replaced the thin conference-announcement pill with a full-width dark banner card: a real date block, title, one-line description, and a proper "Register now" button, instead of a single line of text with an arrow. Added "Get started" and "View on GitHub" buttons to the hero, giving visitors an actual next action instead of only prose. --- assets/main.scss | 212 ++++++++++++++++++++----------- layouts/partials/main/chip.html | 23 ++-- layouts/partials/main/cover.html | 6 + 3 files changed, 159 insertions(+), 82 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index a9bcad8..9eb6c3c 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -346,9 +346,10 @@ body { padding: 2rem 0; } #cover-heading { - margin: 0; + margin: 0 auto; padding: 0; max-width: $maxwidthwide; + text-align: center; #title { #title-text { #title-name { @@ -375,7 +376,7 @@ body { #hero-lede { max-width: 58rem; - margin: 1.5rem 0 0; + margin: 1.5rem auto 0; font-size: 1.1rem; line-height: 1.6; color: $tiletext2; @@ -384,18 +385,58 @@ body { } } + #hero-cta { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 1rem; + margin: 2rem 0 0; + } + + .hero-button { + padding: 0.8rem 1.8rem; + border-radius: 2rem; + font-weight: 700; + text-decoration: none; + transition: all 200ms ease-in-out; + } + + .hero-button-primary { + background-color: black; + color: white; + + &:hover { + background-color: $greyheader; + transform: translateY(-2px); + } + } + + .hero-button-secondary { + background-color: $tilebg; + color: $tiletext; + + &:hover { + background-color: $tilebg2; + transform: translateY(-2px); + } + } + #hero-points { display: flex; flex-wrap: wrap; + justify-content: center; gap: 2rem; max-width: 58rem; - margin: 2rem 0 0; + margin: 2rem auto 0; @media (max-width: 50rem) { gap: 1.25rem; margin-top: 1.5rem; } .hero-point { flex: 1 1 14rem; + display: flex; + flex-direction: column; + align-items: center; } .hero-point-icon { width: 2.25rem; @@ -423,95 +464,118 @@ body { } #chip-announcement { - margin: 0 auto 1.5rem; + margin: 0 auto 2rem; max-width: $maxwidthwide; width: 100%; @media (max-width: 50rem) { - width: 100%; - margin: 0 auto 1.25rem; + margin: 0 auto 1.5rem; } + } + + .conference-banner { display: flex; - justify-content: center; + align-items: center; + gap: 1.75rem; + width: 100%; + padding: 1.5rem 2rem; + border-radius: 1.25rem; + background: linear-gradient(120deg, #0b0b12 0%, #22223a 100%); + color: white; + text-decoration: none; + transition: all 200ms ease-in-out; + &:hover { - cursor: pointer; - } + transform: translateY(-2px); + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.25); - .event-chip { - width: auto; - max-width: 100%; - // Flex items default to min-width: auto, which pins them to their content's intrinsic width instead of letting them shrink. - min-width: 0; - display: inline-flex; - align-items: center; - background-color: transparent; - border-radius: 2.2rem; - padding: 0.1rem 1rem 0.1rem 0; - font-size: 0.8rem; - font-weight: 500; - color: black; - transition: all 0.3s ease; - outline: 1px solid black; + .conference-banner-cta { + background-color: white; + color: black; - @media (max-width: 50rem) { - border-radius: 1.5rem; - padding: 0.05rem 0.6rem 0.05rem 0; - font-size: 0.7rem; - // Lets the date and message stack instead of forcing one nowrap line into horizontal scroll on narrow viewports. - flex-wrap: wrap; - justify-content: center; - row-gap: 0.35rem; + .arrow { + transform: translateX(0.2rem); + } } + } - .event-date { - background-color: black; - color: white; - padding: 0.5rem 1rem; - border-radius: 2rem; - margin: 0.1rem 1rem 0.1rem 0.2rem; - align-self: start; + @media (max-width: 50rem) { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + padding: 1.25rem 1.5rem; + text-align: left; + } + } - @media (max-width: 50rem) { - padding: 0.35rem 0.7rem; - border-radius: 1.5rem; - margin: 0.05rem 0.7rem 0.05rem 0.1rem; - } - } + .conference-banner-date { + display: flex; + flex-direction: column; + align-items: center; + flex-shrink: 0; + line-height: 1.1; + padding-right: 1.75rem; + border-right: 1px solid rgba(255, 255, 255, 0.2); - .event-text { - position: relative; - // Same min-width: auto fix as .event-chip, this time so the text itself can wrap. - min-width: 0; - white-space: normal; + .conference-banner-month { + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: rgba(255, 255, 255, 0.6); + } - .arrow { - display: inline-block; - transition: transform 0.3s ease; - } - } + .conference-banner-day { + font-size: 1.6rem; + font-weight: 800; + } - &:hover { - color: #4557c4; - transform: scale(1.05); + .conference-banner-year { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.6); + } - .event-date { - background: linear-gradient(135deg, #262fb5, #74c8fa); - } + @media (max-width: 50rem) { + flex-direction: row; + align-items: baseline; + gap: 0.4rem; + padding-right: 0; + border-right: none; + } + } - .event-text .arrow { - transform: translateX(0.2rem); - } - } + .conference-banner-body { + flex: 1; + min-width: 0; + + .conference-banner-title { + font-size: 1.15rem; + font-weight: 800; + } - position: relative; + .conference-banner-desc { + margin: 0.2rem 0 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.75); + max-width: 34rem; + } + } - a { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; - } + .conference-banner-cta { + flex-shrink: 0; + padding: 0.7rem 1.4rem; + border-radius: 2rem; + background-color: rgba(255, 255, 255, 0.12); + font-weight: 700; + white-space: nowrap; + transition: all 200ms ease-in-out; + + .arrow { + display: inline-block; + transition: transform 200ms ease; + } + + @media (max-width: 50rem) { + align-self: flex-start; } } diff --git a/layouts/partials/main/chip.html b/layouts/partials/main/chip.html index db2b35f..d3113a7 100644 --- a/layouts/partials/main/chip.html +++ b/layouts/partials/main/chip.html @@ -1,10 +1,17 @@ -
-
-
Oct 12–14, 2026
-
- scverse conference 2026 · Copenhagen, Denmark — registration is open - + +
+
scverse conference 2026
+

+ Three days of talks, posters, and workshops for the single-cell and spatial omics community — Copenhagen, + Denmark. +

+
+
Register now
+
diff --git a/layouts/partials/main/cover.html b/layouts/partials/main/cover.html index cd0b256..0848285 100644 --- a/layouts/partials/main/cover.html +++ b/layouts/partials/main/cover.html @@ -7,6 +7,12 @@

{{ .Title }}

{{ with .Params.hero }}

{{ .text | markdownify }}

+
{{ range .points }}
From 577ac83060b3cc252e40c9ef28da1566e2eeda1e Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 14:33:28 +0200 Subject: [PATCH 16/21] Let the hero lede and value props use the full container width They were still capped at 58rem while the conference banner and demo box above and below them spanned the full width, so the hero read as narrower than the rest of the page. Removed the cap so all three match. --- assets/main.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index 9eb6c3c..8613b6a 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -375,7 +375,6 @@ body { } #hero-lede { - max-width: 58rem; margin: 1.5rem auto 0; font-size: 1.1rem; line-height: 1.6; @@ -426,7 +425,6 @@ body { flex-wrap: wrap; justify-content: center; gap: 2rem; - max-width: 58rem; margin: 2rem auto 0; @media (max-width: 50rem) { gap: 1.25rem; From e1a76edcbd9e95e3e4fd9490b88581f7eb472c4e Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 14:47:22 +0200 Subject: [PATCH 17/21] Trim the banner and hero lede, remove hero CTA buttons, link the value props The conference banner was taller and wider than it needed to be, creating an abrupt jump against the narrower centered title right below it. Reduced its padding and inset it to 92% width. Shortened the hero lede paragraph to one tighter sentence. Removed the "Get started"/"View on GitHub" buttons, which felt arbitrary next to the rest of the hero. Made each "For X" value prop a link instead (researchers -> /learn, developers -> /join, institutions -> /about/mission), so they carry the same navigational purpose without extra buttons competing for attention. --- assets/main.scss | 56 ++++++++++---------------------- content/_index.md | 5 ++- layouts/partials/main/cover.html | 10 ++---- 3 files changed, 23 insertions(+), 48 deletions(-) diff --git a/assets/main.scss b/assets/main.scss index 8613b6a..dbce814 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -384,42 +384,6 @@ body { } } - #hero-cta { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 1rem; - margin: 2rem 0 0; - } - - .hero-button { - padding: 0.8rem 1.8rem; - border-radius: 2rem; - font-weight: 700; - text-decoration: none; - transition: all 200ms ease-in-out; - } - - .hero-button-primary { - background-color: black; - color: white; - - &:hover { - background-color: $greyheader; - transform: translateY(-2px); - } - } - - .hero-button-secondary { - background-color: $tilebg; - color: $tiletext; - - &:hover { - background-color: $tilebg2; - transform: translateY(-2px); - } - } - #hero-points { display: flex; flex-wrap: wrap; @@ -435,6 +399,18 @@ body { display: flex; flex-direction: column; align-items: center; + text-decoration: none; + border-radius: 1rem; + padding: 0.75rem; + transition: background-color 200ms ease-in-out; + + &:hover { + background-color: $tilebg4; + + .hero-point-icon { + transform: scale(1.08); + } + } } .hero-point-icon { width: 2.25rem; @@ -447,6 +423,7 @@ body { font-size: 1.1rem; background-color: $tilebg; color: $tiletext; + transition: transform 200ms ease-in-out; } .hero-point-title { font-weight: 700; @@ -474,9 +451,10 @@ body { display: flex; align-items: center; gap: 1.75rem; - width: 100%; - padding: 1.5rem 2rem; - border-radius: 1.25rem; + width: 92%; + margin: 0 auto; + padding: 1rem 1.75rem; + border-radius: 1rem; background: linear-gradient(120deg, #0b0b12 0%, #22223a 100%); color: white; text-decoration: none; diff --git a/content/_index.md b/content/_index.md index 63815fb..97e77a7 100644 --- a/content/_index.md +++ b/content/_index.md @@ -6,22 +6,25 @@ description = "Foundational tools for single-cell omics data analysis" [hero] text = """\ -scverse is an open-source, community-governed ecosystem of interoperable Python packages for single-cell and spatial omics data analysis — from foundational data structures like AnnData to analysis frameworks like Scanpy, scvi-tools, and Squidpy. The tools share common data formats, so you can move between them in a single analysis instead of gluing together incompatible pipelines.""" +scverse is an open-source, community-governed ecosystem of interoperable Python packages for single-cell and spatial omics analysis — from data structures like AnnData to analysis frameworks like Scanpy, scvi-tools, and Squidpy, all sharing common data formats so you can move between them in one analysis.""" [[hero.points]] icon = "bi-mortarboard" title = "For researchers" text = "Well-documented, actively maintained tools that interoperate out of the box." +link = "/learn" [[hero.points]] icon = "bi-code-slash" title = "For developers" text = "Shared data structures and conventions, so your package fits the ecosystem from day one." +link = "/join" [[hero.points]] icon = "bi-bank" title = "For institutions" text = "A community-governed foundation ensuring the long-term maintenance of critical infrastructure." +link = "/about/mission" # Core packages [packages] diff --git a/layouts/partials/main/cover.html b/layouts/partials/main/cover.html index 0848285..de851f3 100644 --- a/layouts/partials/main/cover.html +++ b/layouts/partials/main/cover.html @@ -7,19 +7,13 @@

{{ .Title }}

{{ with .Params.hero }}

{{ .text | markdownify }}

-
{{ range .points }} - + {{ end }}
{{ end }} From e76f744ab78f370797eb57ad15381c526256cbeb Mon Sep 17 00:00:00 2001 From: Lukas Heumos Date: Thu, 3 Sep 2026 14:52:24 +0200 Subject: [PATCH 18/21] Add a compact social icon row to the navbar GitHub, Discourse, Zulip, Bluesky, X, and YouTube as small icon-only links, reusing the existing bootstrap-icons and custom .i-* icon assets (no new SVGs needed) rather than duplicating the old hero button row we removed earlier. Hidden between 992px and ~1180px: Bootstrap's own collapse breakpoint is 992px, which isn't wide enough to also fit this row, and it was overflowing the nav and clipping the Join button in that range. The footer still lists all of these regardless of width. --- assets/main.scss | 35 +++++++++++++++++++++++++++++++++++ layouts/partials/header.html | 30 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/assets/main.scss b/assets/main.scss index dbce814..cdc3d5b 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -263,6 +263,41 @@ body { } } +.nav-social { + display: flex; + align-items: center; + gap: 0.15rem; + padding: 0.5rem 0.25rem !important; + + // Bootstrap's own collapse breakpoint (992px) isn't wide enough to also fit this row, so it overflows the nav between 992px and roughly 1180px; hide it there and rely on the footer. + @media (max-width: 1199px) { + display: none; + } + + a { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 50%; + color: $navtext; + font-size: 1rem; + transition: all 200ms ease-in-out; + + &:hover { + background-color: $navbghover; + color: $navtexthover; + + .i-discourse::before, + .i-zulip::before, + .i-bluesky::before { + opacity: 1; + } + } + } +} + #join-button { color: white; background-color: #4557c4; diff --git a/layouts/partials/header.html b/layouts/partials/header.html index fbfe646..6bd82be 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -136,6 +136,36 @@ +