Skip to content

Commit db576e4

Browse files
committed
minor fixes
1 parent 4438b8c commit db576e4

3 files changed

Lines changed: 91 additions & 66 deletions

File tree

.vitepress/theme/ZarrHero.vue

Lines changed: 90 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
<script setup lang="ts">
2-
// The SVG is loaded as a raw string (Vite `?raw` import) and rendered
3-
// via v-html so that the inline <style> tag inside the SVG (which
4-
// carries the @keyframes for the chunked-cube reveal) survives Vue's
5-
// template compiler. v-pre alone does NOT prevent the compiler from
6-
// stripping <style>/<script> tags from templates, so we have to bypass
7-
// template compilation entirely.
2+
// The chunked-cube SVG lives at /public/zarr-hero.svg and is loaded
3+
// via fetch() on mount. Two reasons:
4+
// 1. Keeps the home page's inline HTML payload small — the SVG is
5+
// ~60 KB and was previously inlined via `?raw` + v-html.
6+
// 2. Lets the SVG's inline <style> block (which carries the @keyframes
7+
// for the chunk reveal) survive into the DOM — fetch + innerHTML
8+
// preserves <style> tags exactly.
9+
//
10+
// The host div uses `v-once` so Vue does not try to reconcile against
11+
// the imperatively-injected SVG. The popup is a sibling, not a child,
12+
// so Vue still owns that branch.
813
//
914
// Reveal: ~3s one-shot. Each chunk has class="c cN"; the cN class drives
1015
// its individual chunkIn_N keyframe (opacity 0 → 1) and a SMIL translate
1116
// (off-screen → in-place). After the reveal, chunks settle.
1217
//
13-
// Interactivity: hovering a chunk dims the rest of the cube and shows
14-
// a small callout giving that chunk's address (chunk[x,y,z], derived
15-
// from its index in the 3×3×3 grid) and a deterministic 4×4 mini-grid
16-
// representing its "data". Conveys Zarr's value: chunks are
17-
// independently addressable and each one carries its own contents.
18+
// Interactivity: hovering a chunk dims the rest and surfaces a callout
19+
// with the chunk's address (chunk[z, y, x] derived from visual rank in
20+
// the 3×3×3 grid), shape, and codec. Click/tap selects; tapping outside
21+
// the cube dismisses. Conveys: chunks are independently addressable and
22+
// each one carries its own compressed contents.
1823
//
19-
// Click/tap selects the chunk; tapping outside the cube dismisses.
20-
// SSR-safe: handlers attach only on mount.
24+
// SSR-safe: handlers and SVG injection happen only on mount.
2125
2226
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
23-
import svg from './zarr-hero.svg?raw'
27+
import { withBase } from 'vitepress'
2428
2529
interface Hovered {
2630
index: number
2731
rect: DOMRect
2832
}
2933
3034
const wrap = ref<HTMLDivElement | null>(null)
35+
const svgHost = ref<HTMLDivElement | null>(null)
3136
const hovered = ref<Hovered | null>(null)
3237
3338
let currentChunk: Element | null = null
@@ -40,8 +45,8 @@ let cleanup: (() => void) | null = null
4045
let visualRank = new WeakMap<Element, number>()
4146
let visualRankBuilt = false
4247
43-
function buildVisualRank(root: HTMLElement): void {
44-
const chunks = Array.from(root.querySelectorAll<Element>('.c'))
48+
function buildVisualRank(host: HTMLElement): void {
49+
const chunks = Array.from(host.querySelectorAll<Element>('.c'))
4550
const sorted = chunks
4651
.map((el) => ({ el, r: el.getBoundingClientRect() }))
4752
.sort((a, b) => a.r.top - b.r.top || a.r.left - b.r.left)
@@ -55,45 +60,71 @@ function rankCoords(rank: number): [number, number, number] {
5560
return [Math.floor(rank / 9), Math.floor(rank / 3) % 3, rank % 3]
5661
}
5762
58-
// Deterministic per-chunk compression stats. Same seed each hover, so
59-
// the same chunk shows the same numbers — emphasizes that chunks are
60-
// independent units with their own compressed payloads, and that
61-
// compression ratios vary with the data each chunk happens to hold.
62-
function chunkStats(rank: number): { ratio: string } {
63-
let seed = (rank + 1) * 16807
64-
seed = (seed * 1103515245 + 12345) & 0x7fffffff
65-
// 4× – 12×: typical for scientific arrays with zstd + shuffle, the
66-
// pattern used by ARCO-ERA5, CMIP6 cloud archives, and most production
67-
// Zarr stores. Conservative end of what's actually seen in practice;
68-
// sparse / quantized data routinely hits 20×+ but we keep the range
69-
// tight so the popup reads as a credible default, not a brag.
70-
const r = 4 + ((seed % 1000) / 1000) * 8
71-
return { ratio: r.toFixed(1) }
72-
}
73-
7463
const coords = computed(() =>
7564
hovered.value ? rankCoords(hovered.value.index).join(', ') : '',
7665
)
77-
const stats = computed(() =>
78-
hovered.value ? chunkStats(hovered.value.index) : { ratio: '' },
79-
)
8066
67+
// Popup positioning with edge detection. The popup sits 14 px to the
68+
// right of the chunk by default. If that would push it past the viewport
69+
// right edge (e.g. on a narrow mobile viewport), we flip to the left of
70+
// the chunk; if neither fits, we center horizontally in the viewport.
71+
// Vertical: top-align with the chunk; flip below if it would overflow.
8172
const calloutStyle = computed(() => {
8273
if (!hovered.value || !wrap.value) return {}
8374
const w = wrap.value.getBoundingClientRect()
8475
const r = hovered.value.rect
85-
return {
86-
left: `${r.right - w.left + 14}px`,
87-
top: `${r.top - w.top - 8}px`,
76+
77+
// Estimated callout dimensions (matches the rendered min-width and
78+
// typical content height; close enough for placement decisions).
79+
const POPUP_W = 200
80+
const POPUP_H = 90
81+
const GAP = 14
82+
const MARGIN = 8
83+
84+
const vw = typeof window !== 'undefined' ? window.innerWidth : 1200
85+
const vh = typeof window !== 'undefined' ? window.innerHeight : 800
86+
87+
let left: number
88+
if (r.right + GAP + POPUP_W <= vw - MARGIN) {
89+
left = r.right - w.left + GAP
90+
} else if (r.left - GAP - POPUP_W >= MARGIN) {
91+
left = r.left - w.left - GAP - POPUP_W
92+
} else {
93+
// Center horizontally in viewport (very narrow screens).
94+
left = vw / 2 - w.left - POPUP_W / 2
95+
}
96+
97+
let top: number
98+
if (r.top + POPUP_H <= vh - MARGIN) {
99+
top = r.top - w.top - 8
100+
} else {
101+
top = r.bottom - w.top + 8
88102
}
103+
104+
return { left: `${left}px`, top: `${top}px` }
89105
})
90106
91-
onMounted(() => {
107+
onMounted(async () => {
92108
const root = wrap.value
93-
if (!root) return
109+
const host = svgHost.value
110+
if (!root || !host) return
111+
112+
// Fetch and inject the SVG. innerHTML preserves the inline <style>
113+
// block carrying @keyframes; v-once on the host div tells Vue not to
114+
// touch the injected content.
115+
try {
116+
const res = await fetch(withBase('/zarr-hero.svg'))
117+
if (res.ok) {
118+
host.innerHTML = await res.text()
119+
}
120+
} catch {
121+
// Network failure — leave the host empty. The wrapper's aria-label
122+
// still describes what should be here for screen readers.
123+
return
124+
}
94125
95126
const set = (chunk: Element) => {
96-
if (!visualRankBuilt) buildVisualRank(root)
127+
if (!visualRankBuilt) buildVisualRank(host)
97128
if (currentChunk && currentChunk !== chunk) {
98129
currentChunk.classList.remove('is-hovered')
99130
}
@@ -115,29 +146,29 @@ onMounted(() => {
115146
const onOver = (e: PointerEvent) => {
116147
const target = e.target as Element | null
117148
const chunk = target?.closest?.('.c')
118-
if (chunk && root.contains(chunk)) set(chunk)
149+
if (chunk && host.contains(chunk)) set(chunk)
119150
}
120151
const onOut = (e: PointerEvent) => {
121-
// Only clear when leaving the entire wrapper (not when sliding
122-
// between chunks).
123152
const related = e.relatedTarget as Element | null
124153
if (related && root.contains(related)) return
125154
clear()
126155
}
127156
const onClick = (e: Event) => {
128157
const target = e.target as Element | null
129158
const chunk = target?.closest?.('.c')
130-
if (chunk && root.contains(chunk)) set(chunk)
159+
if (chunk && host.contains(chunk)) set(chunk)
131160
else clear()
132161
}
133162
134-
root.addEventListener('pointerover', onOver)
135-
root.addEventListener('pointerout', onOut)
136-
root.addEventListener('click', onClick)
163+
// Listeners attach on the SVG host (where the chunks now live), not
164+
// the wrapper, so the popup itself doesn't intercept hover events.
165+
host.addEventListener('pointerover', onOver)
166+
host.addEventListener('pointerout', onOut)
167+
host.addEventListener('click', onClick)
137168
cleanup = () => {
138-
root.removeEventListener('pointerover', onOver)
139-
root.removeEventListener('pointerout', onOut)
140-
root.removeEventListener('click', onClick)
169+
host.removeEventListener('pointerover', onOver)
170+
host.removeEventListener('pointerout', onOut)
171+
host.removeEventListener('click', onClick)
141172
}
142173
})
143174
@@ -149,8 +180,9 @@ onBeforeUnmount(() => cleanup?.())
149180
ref="wrap"
150181
class="zarr-hero"
151182
:class="{ 'has-hover': hovered }"
183+
aria-label="Zarr animated chunked cube"
152184
>
153-
<div class="zarr-hero__svg" v-html="svg" />
185+
<div ref="svgHost" class="zarr-hero__svg" v-once />
154186
<Transition name="callout">
155187
<div v-if="hovered" class="zarr-callout" :style="calloutStyle">
156188
<div class="zarr-callout__addr">chunk[{{ coords }}]</div>
@@ -161,7 +193,7 @@ onBeforeUnmount(() => cleanup?.())
161193
</div>
162194
<div>
163195
<dt>codec</dt>
164-
<dd>zstd · <strong>{{ stats.ratio }}×</strong> compressed</dd>
196+
<dd>zstd</dd>
165197
</div>
166198
</dl>
167199
</div>
@@ -193,22 +225,22 @@ onBeforeUnmount(() => cleanup?.())
193225
--zarr-glow: rgba(232, 155, 184, 0.7);
194226
}
195227
228+
.zarr-hero__svg {
229+
/* Reserve space so layout doesn't shift while the SVG fetches in. */
230+
aspect-ratio: 560 / 480;
231+
width: 100%;
232+
}
196233
.zarr-hero :deep(svg) {
197234
display: block;
198235
width: 100%;
199236
height: auto;
200237
}
201238
202-
/* Each chunk is interactive — hover/tap to reveal its address and a
203-
data preview in the floating callout. */
204239
.zarr-hero :deep(.c) {
205240
cursor: pointer;
206241
transition: opacity 200ms ease, filter 200ms ease;
207242
}
208243
209-
/* When ANY chunk is hovered/selected, dim the rest so the focused
210-
chunk reads clearly. !important is needed because each chunk has
211-
`animation-fill-mode: forwards` holding its post-reveal opacity. */
212244
.zarr-hero.has-hover :deep(.c:not(.is-hovered)) {
213245
opacity: 0.32 !important;
214246
}
@@ -218,7 +250,6 @@ onBeforeUnmount(() => cleanup?.())
218250
drop-shadow(0 0 14px var(--zarr-glow));
219251
}
220252
221-
/* Floating callout */
222253
.zarr-callout {
223254
position: absolute;
224255
z-index: 10;
@@ -263,10 +294,6 @@ onBeforeUnmount(() => cleanup?.())
263294
color: var(--vp-c-text-2);
264295
line-height: 1.4;
265296
}
266-
.zarr-callout__meta dd strong {
267-
color: var(--vp-c-text-1);
268-
font-weight: 600;
269-
}
270297
271298
.callout-enter-active,
272299
.callout-leave-active {
@@ -278,8 +305,6 @@ onBeforeUnmount(() => cleanup?.())
278305
transform: scale(0.95);
279306
}
280307
281-
/* Reduced motion: disable the reveal stagger and the callout's
282-
scale-in. Hover/tap behaviour stays. */
283308
@media (prefers-reduced-motion: reduce) {
284309
.zarr-hero :deep(.c) {
285310
animation: none !important;

adopters/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const stats = computed(() => {
3737
<PageHero
3838
eyebrow="Adopters"
3939
headline="Used in production by leaders across science and industry."
40-
lead="From climate-scale Earth observation to peta-scale connectomics, Zarr is how teams move large arrays through cloud storage and analysis pipelines."
40+
lead="From peta-scale Earth observation to connectomics imaging, Zarr is how teams move large arrays through cloud storage and analysis pipelines."
4141
>
4242
<template #meta>
4343
<div class="adopters-stats">

0 commit comments

Comments
 (0)