Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| --- | |
| import HtmlEmbed from "../components/HtmlEmbed.astro"; | |
| import Image from "../components/Image.astro"; | |
| import Wide from "../components/Wide.astro"; | |
| import Seo from "../components/Seo.astro"; | |
| import ThemeToggle from "../components/ThemeToggle.astro"; | |
| import { loadAllVisualsFromMDX } from "../utils/extract-embeds.mjs"; | |
| import * as ArticleMod from "../content/article.mdx"; | |
| import "katex/dist/katex.min.css"; | |
| import "../styles/global.css"; | |
| // Import all images from assets/image via glob | |
| // @ts-ignore - Vite glob import | |
| const imageModules = (import.meta as any).glob('../content/assets/image/**/*.{png,jpg,jpeg,gif,webp,svg}', { eager: true }); | |
| // Create a map from filename to image module | |
| const imageMap = new Map(); | |
| for (const [path, module] of Object.entries(imageModules)) { | |
| // Extract filename from path | |
| const filename = path.split('/').pop(); | |
| if (filename) { | |
| imageMap.set(filename, (module as any).default); | |
| // Also map without extension for flexibility | |
| const nameWithoutExt = filename.replace(/\.[^.]+$/, ''); | |
| imageMap.set(nameWithoutExt, (module as any).default); | |
| } | |
| } | |
| // Helper to find image by variable name (which often contains the filename) | |
| function findImage(varName: string) { | |
| // Try direct match with the variable name (which is often the sanitized filename) | |
| // Variable names look like: Screenshot_2025_10_30_at_13_02_36_29c1384e_bcac_80d6_a72d_ff34bc221b60 | |
| // Filenames look like: Screenshot_2025-10-30_at_13_02_36_29c1384e-bcac-80d6-a72d-ff34bc221b60.png | |
| // Try to match by finding a filename that contains most of the variable name parts | |
| for (const [filename, img] of imageMap.entries()) { | |
| // Normalize both for comparison | |
| const normalizedFilename = filename.toLowerCase().replace(/[-_]/g, ''); | |
| const normalizedVar = varName.toLowerCase().replace(/[-_]/g, ''); | |
| if (normalizedFilename.includes(normalizedVar) || normalizedVar.includes(normalizedFilename.replace(/\.[^.]+$/, ''))) { | |
| return img; | |
| } | |
| } | |
| // Fallback: try partial match | |
| const varParts = varName.split('_'); | |
| for (const [filename, img] of imageMap.entries()) { | |
| const matchCount = varParts.filter(part => | |
| part.length > 3 && filename.toLowerCase().includes(part.toLowerCase()) | |
| ).length; | |
| if (matchCount >= 3) { | |
| return img; | |
| } | |
| } | |
| return null; | |
| } | |
| // Import article metadata | |
| const articleFM = (ArticleMod as any).frontmatter ?? {}; | |
| // Keep <br> tags but remove other HTML | |
| const articleTitle = String(articleFM?.title ?? "Article") | |
| .replace(/<(?!br\s*\/?)[^>]+>/gi, "") // Remove all HTML except <br> | |
| .replace(/\\n/g, "<br>") | |
| .trim(); | |
| const articleDesc = articleFM?.description ?? ""; | |
| // Page metadata | |
| const pageTitle = `Visualizations - ${articleTitle}`; | |
| const pageDesc = articleDesc; | |
| // Load all visual elements from the content | |
| const allVisuals = loadAllVisualsFromMDX(); | |
| // Add banner as first item (it's in Hero.astro, not in MDX content) | |
| const bannerItem = { | |
| type: 'banner', // Special type for banner | |
| src: 'banner.html', | |
| frameless: true, | |
| wide: true, | |
| hideCaption: true, // No header for banner | |
| }; | |
| // Filter out skipGallery items and prepend banner | |
| const visuals = [bannerItem, ...allVisuals.filter((item: any) => !item.skipGallery)]; | |
| const embedCount = visuals.filter((v: any) => v.type === 'embed').length; | |
| const imageCount = visuals.filter((v: any) => v.type === 'image').length; | |
| const tableCount = visuals.filter((v: any) => v.type === 'table').length; | |
| // Generate a clean display name for each item (null if no meaningful name) | |
| function getDisplayName(item: any): string | null { | |
| if (item.type === 'embed') { | |
| // Prefer title, else format filename | |
| if (item.title) return item.title; | |
| const filename = item.src?.replace(/\.html$/, '').replace(/^.*\//, '') || ''; | |
| // Convert kebab-case to Title Case | |
| return filename | |
| .split(/[-_]/) | |
| .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join(' '); | |
| } else if (item.type === 'image') { | |
| // Prefer alt text | |
| if (item.alt && item.alt !== 'Image') return item.alt; | |
| if (item.caption) return item.caption; | |
| return null; // No meaningful name | |
| } else if (item.type === 'table') { | |
| // Tables don't have reliable names | |
| return null; | |
| } | |
| return null; | |
| } | |
| // Get type label for display | |
| function getTypeLabel(type: string): string { | |
| switch (type) { | |
| case 'embed': return 'Chart'; | |
| case 'banner': return 'Banner'; | |
| case 'image': return 'Image'; | |
| case 'table': return 'Table'; | |
| default: return 'Visual'; | |
| } | |
| } | |
| // Generate clean anchor slug - only for embeds with a real identifier | |
| function generateSlug(item: any): string | null { | |
| // Only embeds have reliable identifiers (filename) | |
| if (item.type === 'embed' && item.src) { | |
| const filename = item.src.replace(/\.html$/, '').replace(/^.*\//, ''); | |
| return filename | |
| .toLowerCase() | |
| .replace(/[^a-z0-9]+/g, '-') | |
| .replace(/^-+|-+$/g, '') | |
| .substring(0, 40); | |
| } | |
| // No anchor for tables, images without proper ID | |
| return null; | |
| } | |
| // Add anchor IDs and display names to visuals | |
| const usedSlugs = new Set<string>(); | |
| let displayIndex = 0; // Counter for displayed items (excludes banner) | |
| const visualsWithMeta = visuals.map((item: any) => { | |
| const displayName = getDisplayName(item); | |
| let slug = generateSlug(item); | |
| // Handle duplicates by adding a number suffix | |
| if (slug && usedSlugs.has(slug)) { | |
| let counter = 2; | |
| while (usedSlugs.has(`${slug}-${counter}`)) { | |
| counter++; | |
| } | |
| slug = `${slug}-${counter}`; | |
| } | |
| if (slug) usedSlugs.add(slug); | |
| // Only increment index for non-banner items | |
| if (item.type !== 'banner') { | |
| displayIndex++; | |
| } | |
| return { | |
| ...item, | |
| displayName, | |
| anchorId: slug, // null if no identifier | |
| index: displayIndex | |
| }; | |
| }); | |
| --- | |
| <html lang="en" data-theme="light"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <Seo title={pageTitle} description={pageDesc} /> | |
| <script is:inline> | |
| (() => { | |
| try { | |
| const saved = localStorage.getItem("theme"); | |
| const prefersDark = | |
| window.matchMedia && | |
| window.matchMedia("(prefers-color-scheme: dark)") | |
| .matches; | |
| const theme = saved || (prefersDark ? "dark" : "light"); | |
| document.documentElement.setAttribute("data-theme", theme); | |
| } catch {} | |
| })(); | |
| </script> | |
| <script type="module" src="/scripts/color-palettes.js"></script> | |
| <!-- External libraries for embeds --> | |
| <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script> | |
| <script src="/src/scripts/mermaid-zoom.js"></script> | |
| </head> | |
| <body class="dataviz-page"> | |
| <ThemeToggle /> | |
| <section class="content-grid dataviz-layout"> | |
| <main> | |
| <header class="dataviz-header"> | |
| <a href="/" class="back-link">← Back to article</a> | |
| <div class="header-content"> | |
| <p class="header-label">Visual assets from</p> | |
| <h1 class="article-title" set:html={articleTitle} /> | |
| </div> | |
| <div class="header-stats"> | |
| {embedCount > 0 && ( | |
| <div class="stat-card"> | |
| <span class="stat-number">{embedCount}</span> | |
| <span class="stat-label">Charts</span> | |
| </div> | |
| )} | |
| {imageCount > 0 && ( | |
| <div class="stat-card"> | |
| <span class="stat-number">{imageCount}</span> | |
| <span class="stat-label">Images</span> | |
| </div> | |
| )} | |
| {tableCount > 0 && ( | |
| <div class="stat-card"> | |
| <span class="stat-number">{tableCount}</span> | |
| <span class="stat-label">Tables</span> | |
| </div> | |
| )} | |
| </div> | |
| </header> | |
| <div class="dataviz-list"> | |
| {visualsWithMeta.map((item: any) => { | |
| const isWide = item.wide || item.fullWidth; | |
| return ( | |
| <article class={`dataviz-item ${isWide ? 'dataviz-item--wide' : ''}`} data-type={item.type} id={item.anchorId || undefined}> | |
| {!item.hideCaption && ( | |
| <header class="item-header"> | |
| <span class="header-number">#{item.index}</span> | |
| <span class="header-type">{getTypeLabel(item.type)}</span> | |
| {item.displayName && ( | |
| <h2 class="header-title">{item.displayName}</h2> | |
| )} | |
| {(item.desc || item.caption) && ( | |
| <p class="header-desc">{item.desc || item.caption}</p> | |
| )} | |
| {item.anchorId && ( | |
| <a href={`/#${item.anchorId}`} class="header-link"> | |
| View in article → | |
| </a> | |
| )} | |
| </header> | |
| )} | |
| <div class="item-content"> | |
| {(item.type === 'embed' || item.type === 'banner') && !isWide && ( | |
| <HtmlEmbed | |
| src={item.src} | |
| title={item.title} | |
| desc={item.desc} | |
| frameless={item.frameless} | |
| data={item.data} | |
| config={item.config} | |
| /> | |
| )} | |
| {(item.type === 'embed' || item.type === 'banner') && isWide && ( | |
| <Wide> | |
| <HtmlEmbed | |
| src={item.src} | |
| title={item.title} | |
| desc={item.desc} | |
| frameless={item.frameless} | |
| data={item.data} | |
| config={item.config} | |
| /> | |
| </Wide> | |
| )} | |
| {item.type === 'image' && (() => { | |
| const imgSrc = findImage(item.src); | |
| return imgSrc ? ( | |
| <Image | |
| src={imgSrc} | |
| alt={item.alt || 'Image'} | |
| caption={item.caption} | |
| /> | |
| ) : ( | |
| <div class="image-not-found"> | |
| <span class="image-placeholder">Image</span> | |
| <code class="image-ref">{item.src}</code> | |
| </div> | |
| ); | |
| })()} | |
| {item.type === 'table' && ( | |
| <div class="table-scroll"> | |
| <table> | |
| <thead> | |
| <tr> | |
| {item.headers.map((header: string) => ( | |
| <th set:html={header} /> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {item.rows.map((row: string[]) => ( | |
| <tr> | |
| {row.map((cell: string) => ( | |
| <td set:html={cell} /> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| </article> | |
| ); | |
| })} | |
| </div> | |
| </main> | |
| </section> | |
| <script> | |
| // Initialize image zoom | |
| function initializeZoom() { | |
| const zoomableImages = document.querySelectorAll('img[data-zoomable="true"]'); | |
| if ((window as any).mediumZoom && zoomableImages.length > 0) { | |
| zoomableImages.forEach((img) => { | |
| if (!img.classList.contains("medium-zoom-image")) { | |
| try { | |
| (window as any).mediumZoom(img, { | |
| background: "rgba(0,0,0,.85)", | |
| margin: 24, | |
| scrollOffset: 0, | |
| }); | |
| } catch (error) { | |
| console.error("Error initializing zoom:", error); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", initializeZoom); | |
| } else { | |
| initializeZoom(); | |
| } | |
| window.addEventListener("load", () => setTimeout(initializeZoom, 100)); | |
| </script> | |
| <style is:global> | |
| /* Layout */ | |
| .dataviz-layout { | |
| padding-top: 40px; | |
| padding-bottom: 80px; | |
| } | |
| .dataviz-layout main { | |
| grid-column: 2; | |
| } | |
| .dataviz-page #theme-toggle { | |
| left: auto; | |
| right: var(--spacing-3); | |
| } | |
| /* Page Header */ | |
| .dataviz-header { | |
| margin-bottom: 16px; | |
| text-align: center; | |
| } | |
| .back-link { | |
| color: var(--muted-color); | |
| text-decoration: none; | |
| font-size: 0.8125rem; | |
| display: inline-block; | |
| margin-bottom: 20px; | |
| transition: color 0.15s; | |
| } | |
| .back-link:hover { | |
| color: var(--text-color); | |
| } | |
| .header-content { | |
| margin-bottom: 20px; | |
| } | |
| .header-label { | |
| font-size: 0.6875rem; | |
| color: var(--muted-color); | |
| margin: 0 0 8px 0; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| font-weight: 600; | |
| } | |
| .article-title { | |
| font-size: clamp(28px, 4vw, 48px); | |
| font-weight: 800; | |
| margin: 0; | |
| color: var(--text-color); | |
| line-height: 1.1; | |
| letter-spacing: -0.02em; | |
| } | |
| .header-stats { | |
| display: flex; | |
| justify-content: center; | |
| gap: 32px; | |
| flex-wrap: wrap; | |
| } | |
| .stat-card { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 2px; | |
| } | |
| .stat-number { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: var(--text-color); | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .stat-label { | |
| font-size: 0.6875rem; | |
| color: var(--muted-color); | |
| text-transform: uppercase; | |
| letter-spacing: 0.03em; | |
| } | |
| /* List */ | |
| .dataviz-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 56px; | |
| } | |
| /* Item */ | |
| .dataviz-item { | |
| position: relative; | |
| scroll-margin-top: 24px; | |
| } | |
| .dataviz-item .html-embed { | |
| margin: 0 ; | |
| } | |
| .dataviz-item .wide { | |
| /* Keep the wide centering */ | |
| width: min(1100px, 100vw - var(--content-padding-x) * 4); | |
| margin-left: 50%; | |
| transform: translateX(-50%); | |
| /* Remove visual effects */ | |
| padding: 0 ; | |
| background: transparent ; | |
| border-radius: 0 ; | |
| -webkit-mask: none ; | |
| mask: none ; | |
| } | |
| .dataviz-item .wide .html-embed { | |
| margin: 0 ; | |
| } | |
| /* Header */ | |
| .item-header { | |
| margin-bottom: 40px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 2px; | |
| text-align: center; | |
| } | |
| .header-number { | |
| font-size: 0.6875rem; | |
| font-weight: 500; | |
| color: var(--muted-color); | |
| opacity: 0.5; | |
| } | |
| .header-type { | |
| font-size: 0.625rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| color: var(--text-color); | |
| } | |
| .header-title { | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| margin: 2px 0 0 0 ; | |
| padding: 0 ; | |
| color: var(--text-color); | |
| line-height: 1.3; | |
| border: none ; | |
| text-decoration: none ; | |
| } | |
| .header-title::after { | |
| display: none ; | |
| } | |
| .header-desc { | |
| font-size: 0.8125rem; | |
| color: var(--muted-color); | |
| margin: 4px 0 0 0; | |
| max-width: 600px; | |
| line-height: 1.4; | |
| } | |
| .header-link { | |
| margin-top: 4px; | |
| font-size: 0.6875rem; | |
| color: var(--muted-color); | |
| text-decoration: none; | |
| opacity: 0.7; | |
| transition: opacity 0.15s; | |
| } | |
| .header-link:hover { | |
| opacity: 1; | |
| color: var(--primary-color); | |
| } | |
| /* Image not found */ | |
| .image-not-found { | |
| background: var(--neutral-50); | |
| border: 1px dashed var(--border-color); | |
| border-radius: 8px; | |
| padding: 32px 24px; | |
| text-align: center; | |
| } | |
| [data-theme="dark"] .image-not-found { | |
| background: var(--neutral-900); | |
| } | |
| .image-placeholder { | |
| font-size: 0.8125rem; | |
| color: var(--muted-color); | |
| display: block; | |
| margin-bottom: 4px; | |
| } | |
| .image-ref { | |
| font-size: 0.6875rem; | |
| color: var(--muted-color); | |
| opacity: 0.6; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .dataviz-list { | |
| gap: 40px; | |
| } | |
| } | |
| </style> | |
| </body> | |
| </html> | |