Skip to content

Commit e5ffcba

Browse files
committed
add final website fixes
1 parent 785ffd3 commit e5ffcba

5 files changed

Lines changed: 330 additions & 6 deletions

File tree

website/assets/css/style.css

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,3 +2176,203 @@ input[type="range"]::-moz-range-thumb {
21762176
top: 16px;
21772177
left: calc((100vw - 300px) / 2);
21782178
}
2179+
2180+
/* ============================================================
2181+
BOOT OVERLAY
2182+
Full-screen loader shown until the data + chart finish painting.
2183+
On narrow viewports it doubles as a "use a laptop" gate.
2184+
============================================================ */
2185+
2186+
.boot-overlay {
2187+
position: fixed;
2188+
inset: 0;
2189+
z-index: 9999;
2190+
background: var(--bg);
2191+
background-image:
2192+
radial-gradient(
2193+
ellipse 80% 60% at 70% 10%,
2194+
rgba(230, 163, 86, 0.06),
2195+
transparent 70%
2196+
),
2197+
radial-gradient(
2198+
ellipse 60% 50% at 30% 90%,
2199+
rgba(230, 163, 86, 0.03),
2200+
transparent 70%
2201+
);
2202+
color: var(--ink);
2203+
display: flex;
2204+
align-items: center;
2205+
justify-content: center;
2206+
padding: 32px;
2207+
opacity: 1;
2208+
transition: opacity 400ms ease;
2209+
}
2210+
2211+
.boot-overlay.is-hidden {
2212+
opacity: 0;
2213+
pointer-events: none;
2214+
}
2215+
2216+
.boot-inner {
2217+
max-width: 520px;
2218+
width: 100%;
2219+
text-align: center;
2220+
display: flex;
2221+
flex-direction: column;
2222+
align-items: center;
2223+
gap: 32px;
2224+
}
2225+
2226+
.boot-brand {
2227+
display: flex;
2228+
align-items: baseline;
2229+
gap: 10px;
2230+
font-family: var(--display);
2231+
font-weight: 300;
2232+
font-size: 28px;
2233+
letter-spacing: -0.01em;
2234+
}
2235+
2236+
.boot-brand .brand-mark {
2237+
color: var(--accent);
2238+
font-size: 0.7em;
2239+
position: relative;
2240+
top: -2px;
2241+
}
2242+
2243+
.boot-brand .brand-name em {
2244+
color: var(--accent);
2245+
font-style: italic;
2246+
font-weight: 400;
2247+
}
2248+
2249+
/* ------- WIDE STATE (default) ------- */
2250+
2251+
.boot-wide {
2252+
display: flex;
2253+
flex-direction: column;
2254+
align-items: center;
2255+
gap: 18px;
2256+
}
2257+
2258+
.boot-spinner {
2259+
display: flex;
2260+
gap: 8px;
2261+
}
2262+
2263+
.boot-spinner span {
2264+
display: block;
2265+
width: 8px;
2266+
height: 8px;
2267+
border-radius: 50%;
2268+
background: var(--accent);
2269+
animation: bootPulse 1.2s infinite ease-in-out;
2270+
}
2271+
2272+
.boot-spinner span:nth-child(2) { animation-delay: 0.15s; }
2273+
.boot-spinner span:nth-child(3) { animation-delay: 0.30s; }
2274+
2275+
@keyframes bootPulse {
2276+
0%, 80%, 100% { opacity: 0.25; transform: scale(0.85); }
2277+
40% { opacity: 1; transform: scale(1.1); }
2278+
}
2279+
2280+
.boot-status {
2281+
display: flex;
2282+
align-items: center;
2283+
gap: 14px;
2284+
font-family: var(--mono);
2285+
font-size: 11px;
2286+
text-transform: uppercase;
2287+
letter-spacing: 0.12em;
2288+
color: var(--ink-faint);
2289+
}
2290+
2291+
.boot-status-label {
2292+
color: var(--accent);
2293+
font-weight: 500;
2294+
}
2295+
2296+
.boot-status-text {
2297+
color: var(--ink-dim);
2298+
text-transform: none;
2299+
letter-spacing: 0;
2300+
font-style: italic;
2301+
font-family: var(--display);
2302+
font-size: 14px;
2303+
}
2304+
2305+
/* ------- NARROW STATE (hidden by default) ------- */
2306+
2307+
.boot-narrow {
2308+
display: none;
2309+
flex-direction: column;
2310+
align-items: center;
2311+
gap: 18px;
2312+
max-width: 360px;
2313+
}
2314+
2315+
.boot-icon {
2316+
width: 64px;
2317+
height: 48px;
2318+
color: var(--accent);
2319+
}
2320+
2321+
.boot-narrow-title {
2322+
margin: 0;
2323+
font-family: var(--display);
2324+
font-style: italic;
2325+
font-weight: 300;
2326+
font-size: 28px;
2327+
line-height: 1.15;
2328+
color: var(--ink);
2329+
}
2330+
2331+
.boot-narrow-body {
2332+
margin: 0;
2333+
color: var(--ink-dim);
2334+
font-size: 14px;
2335+
line-height: 1.55;
2336+
}
2337+
2338+
.boot-narrow-body strong {
2339+
color: var(--ink);
2340+
font-weight: 500;
2341+
}
2342+
2343+
.boot-url {
2344+
font-family: var(--mono);
2345+
font-size: 12px;
2346+
color: var(--accent);
2347+
background: var(--bg-panel);
2348+
border: 1px solid var(--rule-strong);
2349+
padding: 2px 8px;
2350+
border-radius: 3px;
2351+
white-space: nowrap;
2352+
}
2353+
2354+
.boot-narrow-meta {
2355+
margin-top: 8px;
2356+
font-family: var(--mono);
2357+
font-size: 9.5px;
2358+
text-transform: uppercase;
2359+
letter-spacing: 0.18em;
2360+
color: var(--ink-faint);
2361+
}
2362+
2363+
/* ------- VIEWPORT SWITCH -------
2364+
Below 900px the chart UI is essentially unusable; we show the
2365+
narrow-state copy instead and keep the overlay locked on. */
2366+
@media (max-width: 899px) {
2367+
.boot-wide { display: none; }
2368+
.boot-narrow { display: flex; }
2369+
/* Critical: even after .is-hidden is added by JS, keep the gate up
2370+
because the actual app behind it is broken on this width. */
2371+
.boot-overlay.is-hidden {
2372+
opacity: 1;
2373+
pointer-events: auto;
2374+
}
2375+
/* Also hide the underlying page so nothing flashes behind the gate */
2376+
body.loaded .topbar,
2377+
body.loaded main { display: none; }
2378+
}

website/assets/js/analysis.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,15 @@ function renderAnalysisView() {
125125
analysis.initialized = true;
126126

127127
d3.select("#analysis-to-sandbox").on("click", () => {
128-
const top = immortalsTop(1)[0];
129-
if (top) addToSelection(top);
128+
// Only seed the Sandbox with a starter game when the user has not
129+
// already curated their own selection. Previously this branch always
130+
// appended immortalsTop(1)[0] (deterministically Dota 2), which
131+
// surprised users by mutating their carefully-picked selection every
132+
// time they bounced through Our Reading.
133+
if (state.selectedIds.size === 0) {
134+
const top = immortalsTop(1)[0];
135+
if (top) addToSelection(top);
136+
}
130137
switchView("sandbox");
131138
});
132139

website/assets/js/main.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,14 @@ document.addEventListener("DOMContentLoaded", async () => {
270270
const validViews = ["main", "sandbox", "analysis"];
271271
const hash = window.location.hash.replace("#", "");
272272
if (validViews.includes(hash)) switchView(hash);
273+
274+
// Dismiss the boot overlay once the first paint of the Landscape is
275+
// committed. On narrow viewports CSS keeps the overlay visible (it
276+
// doubles as a "use a laptop" gate), so this is wide-screen-only in
277+
// effect, but we always run it so the markup stays consistent.
278+
requestAnimationFrame(() => {
279+
document.body.classList.add("loaded");
280+
const overlay = document.getElementById("boot-overlay");
281+
if (overlay) overlay.classList.add("is-hidden");
282+
});
273283
});

website/assets/js/sandbox.js

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,18 +343,80 @@ function renderSandbox() {
343343
if (sandbox.colorField === "genre") return colorScale(d.genres[0]);
344344
return colorScale(d[sandbox.colorField]);
345345
});
346-
merged
347-
.select("text")
348-
.text((d) => d.name)
346+
347+
// Labels — place to the right of each bubble by default; flip to the
348+
// left when the full label wouldn't fit; truncate with an ellipsis when
349+
// neither side has room. This keeps every game name fully readable
350+
// inside the sandbox clip rect instead of getting silently cut off.
351+
const LABEL_PAD = 6;
352+
const textSel = merged.select("text").text((d) => d.name);
353+
354+
textSel.each(function (d) {
355+
const node = this;
356+
const fullName = d.name;
357+
// Reset text content before measuring (a previous truncation may
358+
// have left an ellipsised version in the DOM).
359+
node.textContent = fullName;
360+
const w = node.getComputedTextLength();
361+
const cx = sandbox.xScale(d[sandbox.xField]);
362+
const r = rOf(d);
363+
const rightSpace = sandbox.width - cx - r - LABEL_PAD;
364+
const leftSpace = cx - r - LABEL_PAD;
365+
366+
if (w <= rightSpace) {
367+
d.__labelAnchor = "start";
368+
d.__labelX = cx + r + LABEL_PAD;
369+
} else if (w <= leftSpace) {
370+
d.__labelAnchor = "end";
371+
d.__labelX = cx - r - LABEL_PAD;
372+
} else {
373+
// Doesn't fit on either side — truncate on whichever side has more
374+
// room and place there.
375+
if (rightSpace >= leftSpace) {
376+
d.__labelAnchor = "start";
377+
d.__labelX = cx + r + LABEL_PAD;
378+
truncateToWidth(node, fullName, Math.max(0, rightSpace));
379+
} else {
380+
d.__labelAnchor = "end";
381+
d.__labelX = cx - r - LABEL_PAD;
382+
truncateToWidth(node, fullName, Math.max(0, leftSpace));
383+
}
384+
}
385+
});
386+
387+
textSel
388+
.attr("text-anchor", (d) => d.__labelAnchor)
349389
.transition()
350390
.duration(500)
351-
.attr("x", (d) => sandbox.xScale(d[sandbox.xField]) + 14)
391+
.attr("x", (d) => d.__labelX)
352392
.attr("y", (d) => sandbox.yScale(d[sandbox.yField]) + 4);
353393

354394
renderTimeSeries();
355395
updateSandboxDescriptions();
356396
}
357397

398+
// Binary-search shrink the text content of an SVG <text> node until its
399+
// rendered width fits within maxWidth (pixels), appending an ellipsis.
400+
// Used by the sandbox label fitter above. No-op if the full text already
401+
// fits.
402+
function truncateToWidth(textNode, fullText, maxWidth) {
403+
textNode.textContent = fullText;
404+
if (textNode.getComputedTextLength() <= maxWidth) return;
405+
const ellipsis = "…";
406+
let lo = 0;
407+
let hi = fullText.length;
408+
while (lo < hi) {
409+
const mid = (lo + hi + 1) >> 1;
410+
textNode.textContent = fullText.slice(0, mid) + ellipsis;
411+
if (textNode.getComputedTextLength() <= maxWidth) {
412+
lo = mid;
413+
} else {
414+
hi = mid - 1;
415+
}
416+
}
417+
textNode.textContent = lo > 0 ? fullText.slice(0, lo) + ellipsis : ellipsis;
418+
}
419+
358420
d3.selectAll("#sandbox-x, #sandbox-y, #sandbox-color, #sandbox-size").on(
359421
"change",
360422
function () {

website/index.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,51 @@
1010
</head>
1111
<body>
1212

13+
<!-- Boot overlay — covers everything while data loads; on narrow viewports
14+
it doubles as a "desktop only" gate (the .narrow class is added by
15+
CSS based on width). The .loaded class is added to <body> by main.js
16+
once the data + first paint complete; that fades it away on desktop
17+
but keeps the narrow message visible. -->
18+
<div class="boot-overlay" id="boot-overlay" role="status" aria-live="polite">
19+
<div class="boot-inner">
20+
<!-- Brand mark (same diamond + italic name as the topbar) -->
21+
<div class="boot-brand">
22+
<span class="brand-mark"></span>
23+
<span class="brand-name"><em>Built</em> to Last</span>
24+
</div>
25+
26+
<!-- Wide-screen state: spinner + status line -->
27+
<div class="boot-wide">
28+
<div class="boot-spinner" aria-hidden="true">
29+
<span></span><span></span><span></span>
30+
</div>
31+
<div class="boot-status">
32+
<span class="boot-status-label">LOADING</span>
33+
<span class="boot-status-text">preparing the landscape…</span>
34+
</div>
35+
</div>
36+
37+
<!-- Narrow-screen state: "use a laptop" message -->
38+
<div class="boot-narrow">
39+
<svg class="boot-icon" viewBox="0 0 64 48" aria-hidden="true">
40+
<rect x="6" y="4" width="52" height="34" rx="2" fill="none"
41+
stroke="currentColor" stroke-width="2"/>
42+
<rect x="12" y="10" width="40" height="22" rx="1" fill="none"
43+
stroke="currentColor" stroke-width="1.2" opacity="0.45"/>
44+
<path d="M2 42 H62 L58 46 H6 Z" fill="currentColor" opacity="0.85"/>
45+
</svg>
46+
<h1 class="boot-narrow-title">Built for the big screen.</h1>
47+
<p class="boot-narrow-body">
48+
This site is a data-viz study of <strong>12,000+ video games</strong>,
49+
built around a wide bubble chart and a four-axis sandbox. It does
50+
not fit on a phone.<br><br>
51+
Please reopen this page on a laptop or desktop.
52+
</p>
53+
<div class="boot-narrow-meta">COM-480 · DATA VISUALIZATION · EPFL</div>
54+
</div>
55+
</div>
56+
</div>
57+
1358
<!-- Persistent Topbar -->
1459
<header class="topbar">
1560
<div class="topbar-left">

0 commit comments

Comments
 (0)