Buckets:
| from __future__ import annotations | |
| import csv | |
| import html | |
| import json | |
| import shutil | |
| from pathlib import Path | |
| from typing import Any | |
| ROOT = Path(__file__).resolve().parents[1] | |
| DATA_DIR = ROOT / "data" | |
| ITEMS_DIR = DATA_DIR / "items" | |
| SPACE_DIR = ROOT / "space" | |
| ASSETS_DIR = SPACE_DIR / "assets" | |
| DEVICES_DIR = SPACE_DIR / "devices" | |
| CSV_FILES = { | |
| "annual": DATA_DIR / "annual_frontier_2020_2026.csv", | |
| "dominant": DATA_DIR / "dominant_frontier_2020_2026.csv", | |
| "best_price": DATA_DIR / "best_price_per_memory_2020_2026.csv", | |
| "best_bandwidth": DATA_DIR / "best_bandwidth_2020_2026.csv", | |
| "projection": DATA_DIR / "consumer_memory_linear_projection_2020_2030.csv", | |
| "catalog": DATA_DIR / "verified_system_catalog.csv", | |
| "memory_price": DATA_DIR / "memory_price_per_gb.csv", | |
| } | |
| BAND_ORDER = [ | |
| "under_3000", | |
| "3000_5000", | |
| "5000_10000", | |
| "10000_25000", | |
| "25000_75000", | |
| "75000_plus", | |
| ] | |
| BAND_LABELS = { | |
| "under_3000": "Under $3,000", | |
| "3000_5000": "$3,000-$5,000", | |
| "5000_10000": "$5,000-$10,000", | |
| "10000_25000": "$10,000-$25,000", | |
| "25000_75000": "$25,000-$75,000", | |
| "75000_plus": "$75,000+", | |
| } | |
| PRODUCER_ORDER = ["Apple", "NVIDIA", "AMD"] | |
| def read_csv(path: Path) -> list[dict[str, str]]: | |
| with path.open(newline="", encoding="utf-8") as f: | |
| return list(csv.DictReader(f)) | |
| def read_items() -> list[dict[str, Any]]: | |
| return [ | |
| json.loads(path.read_text(encoding="utf-8")) | |
| for path in sorted(ITEMS_DIR.glob("*.json")) | |
| ] | |
| def as_number(value: Any) -> int | float | None: | |
| if value in ("", None): | |
| return None | |
| if isinstance(value, (int, float)): | |
| return value | |
| text = str(value) | |
| try: | |
| parsed = float(text) | |
| except ValueError: | |
| return None | |
| if parsed.is_integer(): | |
| return int(parsed) | |
| return parsed | |
| def normalize_row(row: dict[str, str]) -> dict[str, Any]: | |
| normalized: dict[str, Any] = {} | |
| for key, value in row.items(): | |
| if key in { | |
| "year", | |
| "start_year", | |
| "end_year", | |
| "price_usd", | |
| "memory_gb", | |
| "bandwidth_gbps", | |
| "frontier_set_year", | |
| "fit_base_year", | |
| }: | |
| normalized[key] = as_number(value) | |
| elif key in { | |
| "metric_value", | |
| "price_per_memory_gb", | |
| "linear_fit_memory_gb", | |
| "observed_best_so_far_memory_gb", | |
| "slope_gb_per_year", | |
| "fit_base_memory_gb", | |
| }: | |
| normalized[key] = as_number(value) | |
| elif key in {"carried_forward", "is_projection"}: | |
| normalized[key] = value == "True" | |
| else: | |
| normalized[key] = value | |
| return normalized | |
| def item_price_summary(item: dict[str, Any]) -> dict[str, Any]: | |
| history = sorted( | |
| item["price_history"], | |
| key=lambda event: (event["end_year"], event["start_year"], event["catalog_order"]), | |
| ) | |
| latest = history[-1] | |
| prices = [event["price_usd"] for event in history] | |
| return { | |
| "latest_price_usd": latest["price_usd"], | |
| "latest_price_band": latest["price_band"], | |
| "latest_price_band_label": BAND_LABELS[latest["price_band"]], | |
| "latest_price_kind": latest["price_kind"], | |
| "latest_year": latest["end_year"], | |
| "min_price_usd": min(prices), | |
| "max_price_usd": max(prices), | |
| "price_event_count": len(history), | |
| } | |
| def build_payload() -> dict[str, Any]: | |
| raw_items = read_items() | |
| csv_payload = { | |
| name: [normalize_row(row) for row in read_csv(path)] | |
| for name, path in CSV_FILES.items() | |
| } | |
| catalog_by_item: dict[str, list[dict[str, Any]]] = {} | |
| for row in csv_payload["catalog"]: | |
| catalog_by_item.setdefault(row["item_id"], []).append(row) | |
| items: list[dict[str, Any]] = [] | |
| for item in raw_items: | |
| price_summary = item_price_summary(item) | |
| catalog_rows = sorted( | |
| catalog_by_item.get(item["id"], []), | |
| key=lambda row: (row.get("start_year") or 0, row.get("price_usd") or 0), | |
| ) | |
| items.append({ | |
| "id": item["id"], | |
| "producer": item["producer"], | |
| "name": item["name"], | |
| "item_kind": item["item_kind"], | |
| "memory": item["memory"], | |
| "accelerators": item.get("accelerators", []), | |
| "spec_sources": item.get("spec_sources", []), | |
| "price_history": item["price_history"], | |
| "catalog_rows": catalog_rows, | |
| **price_summary, | |
| }) | |
| items.sort(key=lambda item: ( | |
| PRODUCER_ORDER.index(item["producer"]), | |
| -(item["memory"]["capacity_gb"]), | |
| item["name"], | |
| )) | |
| latest_catalog = [ | |
| row for row in csv_payload["catalog"] | |
| if row.get("end_year") == 2026 | |
| ] | |
| max_memory = max(item["memory"]["capacity_gb"] for item in items) | |
| max_bandwidth = max( | |
| row["bandwidth_gbps"] for row in latest_catalog | |
| if row.get("bandwidth_gbps") is not None | |
| ) | |
| best_under_5k = max( | |
| row["memory_gb"] for row in latest_catalog | |
| if row["price_band"] in {"under_3000", "3000_5000"} | |
| and row.get("memory_gb") is not None | |
| ) | |
| return { | |
| "generated_at": "2026-06-26", | |
| "bands": [{"key": key, "label": BAND_LABELS[key]} for key in BAND_ORDER], | |
| "producers": PRODUCER_ORDER, | |
| "items": items, | |
| "stats": { | |
| "item_count": len(items), | |
| "catalog_row_count": len(csv_payload["catalog"]), | |
| "max_memory_gb": max_memory, | |
| "max_bandwidth_gbps": max_bandwidth, | |
| "best_under_5k_memory_gb": best_under_5k, | |
| }, | |
| **csv_payload, | |
| } | |
| def json_script(payload: dict[str, Any]) -> str: | |
| return ( | |
| "window.LOCAL_FRONTIER_DATA = " | |
| + json.dumps(payload, ensure_ascii=False, separators=(",", ":")) | |
| + ";\n" | |
| ) | |
| def write(path: Path, content: str) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(content, encoding="utf-8") | |
| def page_shell( | |
| *, | |
| title: str, | |
| description: str, | |
| body: str, | |
| asset_prefix: str = "", | |
| page: str = "index", | |
| device_id: str | None = None, | |
| ) -> str: | |
| escaped_title = html.escape(title) | |
| escaped_description = html.escape(description) | |
| device_script = ( | |
| f"\n<script>window.LOCAL_FRONTIER_DEVICE_ID = {json.dumps(device_id)};</script>" | |
| if device_id else "" | |
| ) | |
| return f"""<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>{escaped_title}</title> | |
| <meta name="description" content="{escaped_description}"> | |
| <link rel="stylesheet" href="{asset_prefix}assets/app.css"> | |
| </head> | |
| <body data-page="{page}"> | |
| {body} | |
| <script src="{asset_prefix}assets/local-frontier-data.js"></script>{device_script} | |
| <script src="{asset_prefix}assets/app.js"></script> | |
| </body> | |
| </html> | |
| """ | |
| def index_body() -> str: | |
| return """<div class="site-shell"> | |
| <header class="masthead"> | |
| <nav class="top-nav" aria-label="Primary"> | |
| <a class="wordmark" href="index.html">Local Frontier</a> | |
| <div class="nav-links"> | |
| <a href="#frontier">Frontier</a> | |
| <a href="#database">Database</a> | |
| <a href="#method">Method</a> | |
| </div> | |
| </nav> | |
| <div class="masthead-grid"> | |
| <div> | |
| <p class="eyebrow">Single-machine AI hardware database</p> | |
| <h1>Local Frontier Database</h1> | |
| </div> | |
| <div class="lede"> | |
| <p>This is the interactive version of the Local Hardware Frontier writeup. It tracks Apple, NVIDIA, and AMD systems from 2020-2026 by complete-machine price band, memory capacity, bandwidth, and complete-system USD per GB.</p> | |
| <p>Every chart point links back to a source-backed item page. Apple unified memory is included as first-class accelerator-accessible memory, not treated as an afterthought.</p> | |
| </div> | |
| </div> | |
| <div class="stat-strip" id="statStrip" aria-label="Dataset summary"></div> | |
| </header> | |
| <main> | |
| <section class="frontier-panel" id="frontier" aria-labelledby="frontier-title"> | |
| <div class="panel-heading"> | |
| <div> | |
| <p class="eyebrow">Interactive chart</p> | |
| <h2 id="frontier-title">Frontier Explorer</h2> | |
| </div> | |
| <p>Switch metric and price band. Hover for source rows; click a point to open its item page when the point comes from a specific device.</p> | |
| </div> | |
| <div class="control-grid"> | |
| <label>Metric | |
| <select id="metricSelect"> | |
| <option value="annual_memory">Annual memory by producer</option> | |
| <option value="annual_price_per_gb">Annual complete-system USD/GB</option> | |
| <option value="annual_bandwidth">Annual memory bandwidth</option> | |
| <option value="best_memory">Best-so-far memory</option> | |
| <option value="best_price_per_gb">Best-so-far complete-system USD/GB</option> | |
| <option value="best_bandwidth">Best-so-far memory bandwidth</option> | |
| <option value="projection">Consumer memory projection to 2030</option> | |
| </select> | |
| </label> | |
| <label>Price band | |
| <select id="bandSelect"></select> | |
| </label> | |
| <div class="producer-toggle" id="producerToggle" aria-label="Producer toggles"></div> | |
| </div> | |
| <div id="frontierChart" class="chart-host" aria-live="polite"></div> | |
| </section> | |
| <section class="section-grid intro-grid" aria-labelledby="what-this-tracks"> | |
| <div> | |
| <p class="eyebrow">Scope</p> | |
| <h2 id="what-this-tracks">What This Tracks</h2> | |
| </div> | |
| <div class="prose"> | |
| <p>The data tracks accelerator-accessible memory in one physical machine, grouped by complete-machine price band. It covers Apple, NVIDIA, and AMD from 2020 through 2026.</p> | |
| <p>The core question is practical: what can someone buy or configure locally, and how do affordability, capacity, bandwidth, and vendor tradeoffs move over time?</p> | |
| </div> | |
| </section> | |
| <section class="symbol-band" aria-labelledby="symbols"> | |
| <div> | |
| <p class="eyebrow">Notation</p> | |
| <h2 id="symbols">How To Read The Symbols</h2> | |
| </div> | |
| <dl class="symbol-list"> | |
| <div><dt>U</dt><dd>Unified or coherent memory available to the accelerator.</dd></div> | |
| <div><dt>V</dt><dd>VRAM on one discrete GPU.</dd></div> | |
| <div><dt>Σ</dt><dd>Aggregate installed VRAM across multiple GPUs in one chassis.</dd></div> | |
| <div><dt>*</dt><dd>Price band inferred from a documented component bill of materials.</dd></div> | |
| </dl> | |
| <p class="note">Aggregate VRAM is useful, but it is not the same thing as one unified memory pool. Treat aggregate points as chassis capacity, not a guarantee that one process can use it as one contiguous model memory space.</p> | |
| </section> | |
| <section class="section-grid" aria-labelledby="main-read"> | |
| <div> | |
| <p class="eyebrow">Readout</p> | |
| <h2 id="main-read">Main Read</h2> | |
| </div> | |
| <div class="prose multi-column"> | |
| <p>Under $3,000, AMD changes the shape of the local market in 2025 and 2026 with 128GB unified memory through Framework Desktop-class hardware. Apple is strong on usable unified memory, but the current sub-$3,000 Apple ceiling in this dataset is 64GB. NVIDIA stays at 24GB in this band because the data tracks complete machines, not used cards or component-only builds.</p> | |
| <p>In the $3,000-$5,000 band, both AMD and NVIDIA reach 128GB unified or coherent memory by 2025-2026. This is the band that matters most for consumer and prosumer local model work because it is expensive but still within a serious personal hardware budget.</p> | |
| <p>In the $5,000-$10,000 band, Apple has the most dramatic historical point: the 512GB M3 Ultra Mac Studio launch configuration in 2025. The best-so-far memory charts keep that 512GB point through 2026 because the historical frontier should not go down just because the current Apple configuration changed.</p> | |
| <p>Above $75,000, NVIDIA dominates the memory and bandwidth frontier. DGX Station-class systems are local in the physical sense, but not local in the affordability sense.</p> | |
| </div> | |
| </section> | |
| <section class="database-panel" id="database" aria-labelledby="database-title"> | |
| <div class="panel-heading"> | |
| <div> | |
| <p class="eyebrow">Device pages</p> | |
| <h2 id="database-title">Local Frontier Database</h2> | |
| </div> | |
| <p>A TechPowerUp-style catalog for complete local systems and accelerator configurations, including Apple unified-memory machines.</p> | |
| </div> | |
| <div class="table-controls"> | |
| <label>Search | |
| <input id="deviceSearch" type="search" placeholder="Mac Studio, DGX, W7900, 128GB"> | |
| </label> | |
| <label>Producer | |
| <select id="producerFilter"> | |
| <option value="all">All producers</option> | |
| </select> | |
| </label> | |
| <label>Price band | |
| <select id="tableBandFilter"> | |
| <option value="all">All bands</option> | |
| </select> | |
| </label> | |
| </div> | |
| <div id="deviceTable" class="table-wrap"></div> | |
| </section> | |
| <section class="section-grid" id="method" aria-labelledby="method-title"> | |
| <div> | |
| <p class="eyebrow">Reproducibility</p> | |
| <h2 id="method-title">Method</h2> | |
| </div> | |
| <div class="prose"> | |
| <p>The editable source of truth is <code>data/items/*.json</code>. The generator expands each item price history into CSVs, derives annual and best-so-far frontiers, and renders the static Space from those generated artifacts.</p> | |
| <p>The complete-system price-per-memory metric is total configured machine price divided by accelerator-accessible memory. The extracted component memory-price file is separate and should not be confused with complete-machine affordability.</p> | |
| <p>The projection view fits simple least-squares linear trends to 2020-2026 best-so-far memory series. It is a scenario view, not verified future product data.</p> | |
| </div> | |
| </section> | |
| </main> | |
| </div>""" | |
| def device_body(item: dict[str, Any]) -> str: | |
| escaped_name = html.escape(item["name"]) | |
| return f"""<div class="site-shell device-shell"> | |
| <header class="device-header"> | |
| <nav class="top-nav" aria-label="Primary"> | |
| <a class="wordmark" href="../../index.html">Local Frontier</a> | |
| <div class="nav-links"> | |
| <a href="../../index.html#frontier">Frontier</a> | |
| <a href="../../index.html#database">Database</a> | |
| </div> | |
| </nav> | |
| <a class="back-link" href="../../index.html#database">← Back to database</a> | |
| <p class="eyebrow">{html.escape(item["producer"])}</p> | |
| <h1>{escaped_name}</h1> | |
| <div id="deviceHero" class="device-hero"></div> | |
| </header> | |
| <main id="devicePage" class="device-main"></main> | |
| </div>""" | |
| CSS = r""":root { | |
| color-scheme: dark; | |
| --bg: oklch(16% 0.012 244); | |
| --panel: oklch(21% 0.014 244); | |
| --panel-2: oklch(25% 0.016 244); | |
| --text: oklch(92% 0.015 82); | |
| --muted: oklch(70% 0.018 82); | |
| --faint: oklch(53% 0.018 244); | |
| --line: oklch(34% 0.018 244); | |
| --line-strong: oklch(45% 0.025 244); | |
| --apple: oklch(78% 0.11 195); | |
| --nvidia: oklch(76% 0.16 145); | |
| --amd: oklch(72% 0.18 29); | |
| --accent: oklch(82% 0.14 82); | |
| --blue: oklch(70% 0.14 245); | |
| --green: oklch(74% 0.15 153); | |
| --red: oklch(68% 0.18 27); | |
| --shadow: 0 20px 70px rgb(0 0 0 / 0.24); | |
| --space-1: 4px; | |
| --space-2: 8px; | |
| --space-3: 12px; | |
| --space-4: 16px; | |
| --space-5: 24px; | |
| --space-6: 32px; | |
| --space-7: 48px; | |
| --space-8: 64px; | |
| font-family: "Aptos", "Segoe UI", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| } | |
| a { | |
| color: inherit; | |
| text-decoration-color: color-mix(in oklch, currentColor 45%, transparent); | |
| text-underline-offset: 0.18em; | |
| } | |
| button, | |
| input, | |
| select { | |
| font: inherit; | |
| } | |
| code, | |
| table, | |
| .eyebrow, | |
| .stat-value, | |
| .device-table { | |
| font-family: "Aptos Mono", "SFMono-Regular", Consolas, ui-monospace, monospace; | |
| } | |
| .site-shell { | |
| width: min(1480px, calc(100vw - 32px)); | |
| margin: 0 auto; | |
| } | |
| .masthead { | |
| padding: var(--space-5) 0 var(--space-5); | |
| } | |
| .device-header { | |
| padding: var(--space-5) 0 var(--space-7); | |
| } | |
| .top-nav { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: var(--space-5); | |
| padding: var(--space-3) 0 var(--space-6); | |
| border-bottom: 1px solid var(--line); | |
| } | |
| .wordmark { | |
| font-weight: 800; | |
| letter-spacing: 0.02em; | |
| text-transform: uppercase; | |
| text-decoration: none; | |
| } | |
| .nav-links { | |
| display: flex; | |
| gap: var(--space-5); | |
| color: var(--muted); | |
| font-size: 0.94rem; | |
| } | |
| .masthead-grid { | |
| display: grid; | |
| grid-template-columns: minmax(300px, 0.75fr) minmax(320px, 0.9fr); | |
| gap: clamp(var(--space-5), 4vw, var(--space-7)); | |
| align-items: end; | |
| padding-top: var(--space-5); | |
| } | |
| h1, | |
| h2, | |
| h3, | |
| p { | |
| margin: 0; | |
| } | |
| h1 { | |
| max-width: 18ch; | |
| font-size: clamp(2.35rem, 5.2vw, 5rem); | |
| line-height: 0.95; | |
| letter-spacing: 0; | |
| } | |
| h2 { | |
| font-size: clamp(1.5rem, 2.4vw, 2.35rem); | |
| line-height: 1.02; | |
| letter-spacing: 0; | |
| } | |
| h3 { | |
| font-size: 1.05rem; | |
| line-height: 1.2; | |
| } | |
| .eyebrow { | |
| margin-bottom: var(--space-3); | |
| color: var(--accent); | |
| font-size: 0.76rem; | |
| font-weight: 800; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| } | |
| .lede { | |
| display: grid; | |
| gap: var(--space-4); | |
| color: var(--muted); | |
| font-size: 1.04rem; | |
| line-height: 1.65; | |
| } | |
| .stat-strip { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 1px; | |
| margin-top: var(--space-5); | |
| background: var(--line); | |
| border: 1px solid var(--line); | |
| } | |
| .stat-cell { | |
| min-width: 0; | |
| min-height: 88px; | |
| padding: var(--space-4) var(--space-5); | |
| background: var(--panel); | |
| } | |
| .stat-value { | |
| display: block; | |
| color: var(--text); | |
| overflow-wrap: anywhere; | |
| font-size: clamp(1.3rem, 2.4vw, 2.15rem); | |
| line-height: 1; | |
| } | |
| .stat-label { | |
| display: block; | |
| margin-top: var(--space-3); | |
| color: var(--muted); | |
| font-size: 0.86rem; | |
| } | |
| main { | |
| display: grid; | |
| gap: var(--space-7); | |
| padding-bottom: var(--space-8); | |
| } | |
| .section-grid, | |
| .symbol-band, | |
| .frontier-panel, | |
| .database-panel { | |
| border-top: 1px solid var(--line); | |
| padding-top: var(--space-6); | |
| } | |
| .section-grid { | |
| display: grid; | |
| grid-template-columns: 280px minmax(0, 1fr); | |
| gap: var(--space-7); | |
| } | |
| .prose { | |
| display: grid; | |
| gap: var(--space-4); | |
| max-width: 82ch; | |
| color: var(--muted); | |
| line-height: 1.66; | |
| } | |
| .multi-column { | |
| column-width: 34ch; | |
| display: block; | |
| } | |
| .multi-column p { | |
| break-inside: avoid; | |
| margin-bottom: var(--space-4); | |
| } | |
| .symbol-band { | |
| display: grid; | |
| grid-template-columns: 280px 1fr; | |
| gap: var(--space-7); | |
| } | |
| .symbol-list { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 1px; | |
| margin: 0; | |
| background: var(--line); | |
| border: 1px solid var(--line); | |
| } | |
| .symbol-list div { | |
| padding: var(--space-5); | |
| background: var(--panel); | |
| } | |
| .symbol-list dt { | |
| margin-bottom: var(--space-3); | |
| font-size: 2rem; | |
| font-weight: 900; | |
| } | |
| .symbol-list dd { | |
| margin: 0; | |
| color: var(--muted); | |
| line-height: 1.45; | |
| } | |
| .note { | |
| grid-column: 2; | |
| max-width: 82ch; | |
| color: var(--faint); | |
| line-height: 1.6; | |
| } | |
| .frontier-panel, | |
| .database-panel { | |
| display: grid; | |
| gap: var(--space-5); | |
| min-width: 0; | |
| } | |
| .panel-heading { | |
| display: grid; | |
| grid-template-columns: minmax(260px, 0.4fr) minmax(0, 0.65fr); | |
| gap: var(--space-6); | |
| align-items: end; | |
| } | |
| .panel-heading p { | |
| color: var(--muted); | |
| line-height: 1.55; | |
| } | |
| .control-grid, | |
| .table-controls { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(180px, 1fr)); | |
| gap: var(--space-3); | |
| align-items: end; | |
| } | |
| label { | |
| display: grid; | |
| gap: var(--space-2); | |
| color: var(--muted); | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| } | |
| select, | |
| input { | |
| width: 100%; | |
| min-height: 42px; | |
| border: 1px solid var(--line-strong); | |
| border-radius: 6px; | |
| background: var(--panel); | |
| color: var(--text); | |
| padding: 0 var(--space-3); | |
| letter-spacing: 0; | |
| text-transform: none; | |
| } | |
| select:focus, | |
| input:focus, | |
| button:focus-visible, | |
| a:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 3px; | |
| } | |
| .producer-toggle { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: var(--space-2); | |
| } | |
| .chip { | |
| min-height: 42px; | |
| border: 1px solid var(--line-strong); | |
| border-radius: 999px; | |
| background: var(--panel); | |
| color: var(--muted); | |
| padding: 0 var(--space-4); | |
| cursor: pointer; | |
| } | |
| .chip[aria-pressed="true"] { | |
| color: var(--text); | |
| border-color: color-mix(in oklch, var(--accent) 55%, var(--line-strong)); | |
| background: color-mix(in oklch, var(--accent) 12%, var(--panel)); | |
| } | |
| .chart-host { | |
| position: relative; | |
| min-height: 520px; | |
| border: 1px solid var(--line); | |
| background: var(--panel); | |
| box-shadow: var(--shadow); | |
| } | |
| .chart-host svg { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| } | |
| .chart-title { | |
| padding: var(--space-5) var(--space-5) 0; | |
| } | |
| .chart-subtitle { | |
| margin-top: var(--space-2); | |
| color: var(--muted); | |
| font-size: 0.92rem; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| min-width: 220px; | |
| max-width: 360px; | |
| pointer-events: none; | |
| border: 1px solid var(--line-strong); | |
| background: oklch(18% 0.014 244 / 0.96); | |
| color: var(--text); | |
| padding: var(--space-3); | |
| box-shadow: var(--shadow); | |
| transform: translate(-50%, calc(-100% - 18px)); | |
| line-height: 1.35; | |
| z-index: 5; | |
| } | |
| .tooltip strong { | |
| display: block; | |
| margin-bottom: var(--space-2); | |
| } | |
| .tooltip span { | |
| display: block; | |
| color: var(--muted); | |
| } | |
| .table-wrap { | |
| overflow-x: auto; | |
| border: 1px solid var(--line); | |
| background: var(--panel); | |
| min-width: 0; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.9rem; | |
| } | |
| th, | |
| td { | |
| border-bottom: 1px solid var(--line); | |
| padding: 12px 14px; | |
| text-align: left; | |
| vertical-align: top; | |
| } | |
| th { | |
| position: sticky; | |
| top: 0; | |
| z-index: 1; | |
| background: var(--panel-2); | |
| color: var(--muted); | |
| font-size: 0.72rem; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| } | |
| td { | |
| color: var(--muted); | |
| } | |
| td strong, | |
| .device-link { | |
| color: var(--text); | |
| } | |
| .pill { | |
| display: inline-flex; | |
| align-items: center; | |
| min-height: 24px; | |
| border: 1px solid var(--line-strong); | |
| border-radius: 999px; | |
| padding: 2px 8px; | |
| color: var(--muted); | |
| font-size: 0.76rem; | |
| white-space: nowrap; | |
| } | |
| .producer-Apple { | |
| color: var(--apple); | |
| } | |
| .producer-NVIDIA { | |
| color: var(--nvidia); | |
| } | |
| .producer-AMD { | |
| color: var(--amd); | |
| } | |
| .back-link { | |
| display: inline-flex; | |
| margin-top: var(--space-6); | |
| margin-bottom: var(--space-5); | |
| color: var(--muted); | |
| font-size: 0.94rem; | |
| } | |
| .device-header h1 { | |
| max-width: 22ch; | |
| font-size: clamp(2.3rem, 6vw, 5.4rem); | |
| line-height: 0.96; | |
| } | |
| .device-hero { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 1px; | |
| margin-top: var(--space-6); | |
| background: var(--line); | |
| border: 1px solid var(--line); | |
| } | |
| .device-hero .stat-value { | |
| font-size: clamp(1.15rem, 2.3vw, 2rem); | |
| } | |
| .device-main { | |
| display: grid; | |
| grid-template-columns: minmax(0, 0.9fr) minmax(320px, 0.45fr); | |
| gap: var(--space-7); | |
| align-items: start; | |
| min-width: 0; | |
| } | |
| .detail-block { | |
| display: grid; | |
| gap: var(--space-4); | |
| border-top: 1px solid var(--line); | |
| padding-top: var(--space-5); | |
| min-width: 0; | |
| } | |
| .detail-card { | |
| border: 1px solid var(--line); | |
| background: var(--panel); | |
| padding: var(--space-5); | |
| min-width: 0; | |
| } | |
| .detail-list { | |
| display: grid; | |
| gap: var(--space-3); | |
| margin: 0; | |
| } | |
| .detail-list div { | |
| display: grid; | |
| grid-template-columns: 160px 1fr; | |
| gap: var(--space-4); | |
| } | |
| .detail-list dt { | |
| color: var(--faint); | |
| } | |
| .detail-list dd { | |
| margin: 0; | |
| color: var(--muted); | |
| } | |
| .source-list { | |
| display: grid; | |
| gap: var(--space-3); | |
| margin: 0; | |
| padding: 0; | |
| list-style: none; | |
| } | |
| .source-list li { | |
| color: var(--muted); | |
| line-height: 1.45; | |
| } | |
| .empty { | |
| padding: var(--space-6); | |
| color: var(--muted); | |
| } | |
| @media (max-width: 980px) { | |
| .masthead-grid, | |
| .section-grid, | |
| .symbol-band, | |
| .panel-heading, | |
| .device-main { | |
| grid-template-columns: 1fr; | |
| } | |
| .note { | |
| grid-column: auto; | |
| } | |
| .stat-strip, | |
| .symbol-list, | |
| .device-hero, | |
| .control-grid, | |
| .table-controls { | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| } | |
| } | |
| @media (max-width: 640px) { | |
| .site-shell { | |
| width: min(100% - 20px, 1480px); | |
| } | |
| .top-nav, | |
| .nav-links { | |
| align-items: flex-start; | |
| flex-direction: column; | |
| } | |
| h1 { | |
| font-size: clamp(2.15rem, 11vw, 3.2rem); | |
| } | |
| .device-header h1 { | |
| font-size: clamp(2rem, 10vw, 3rem); | |
| } | |
| .stat-strip, | |
| .symbol-list, | |
| .device-hero, | |
| .control-grid, | |
| .table-controls { | |
| grid-template-columns: 1fr; | |
| } | |
| .chart-host { | |
| min-height: 430px; | |
| } | |
| .detail-list div { | |
| grid-template-columns: 1fr; | |
| gap: var(--space-1); | |
| } | |
| table { | |
| font-size: 0.82rem; | |
| } | |
| } | |
| """ | |
| JS = r"""const DATA = window.LOCAL_FRONTIER_DATA; | |
| const byId = new Map(DATA.items.map((item) => [item.id, item])); | |
| const producers = DATA.producers; | |
| const producerColors = { | |
| Apple: "var(--apple)", | |
| NVIDIA: "var(--nvidia)", | |
| AMD: "var(--amd)", | |
| "Best-so-far": "var(--accent)", | |
| "Under 3k observed": "var(--blue)", | |
| "Under 3k fit": "var(--blue)", | |
| "3k-5k observed": "var(--green)", | |
| "3k-5k fit": "var(--green)", | |
| "5k-10k observed": "var(--red)", | |
| "5k-10k fit": "var(--red)" | |
| }; | |
| function $(selector, root = document) { | |
| return root.querySelector(selector); | |
| } | |
| function escapeHTML(value) { | |
| return String(value ?? "").replace(/[&<>"']/g, (char) => ({ | |
| "&": "&", | |
| "<": "<", | |
| ">": ">", | |
| '"': """, | |
| "'": "'" | |
| })[char]); | |
| } | |
| function fmtMoney(value) { | |
| if (value === null || value === undefined || value === "") return "-"; | |
| return `$${Number(value).toLocaleString("en-US", { maximumFractionDigits: 0 })}`; | |
| } | |
| function fmtNumber(value, digits = 0) { | |
| if (value === null || value === undefined || value === "") return "-"; | |
| return Number(value).toLocaleString("en-US", { | |
| maximumFractionDigits: digits, | |
| minimumFractionDigits: 0 | |
| }); | |
| } | |
| function fmtGB(value) { | |
| return value === null || value === undefined || value === "" ? "-" : `${fmtNumber(value)} GB`; | |
| } | |
| function fmtBandwidth(value) { | |
| return value === null || value === undefined || value === "" ? "-" : `${fmtNumber(value)} GB/s`; | |
| } | |
| function fmtUsdPerGB(value) { | |
| return value === null || value === undefined || value === "" ? "-" : `$${fmtNumber(value, 2)}/GB`; | |
| } | |
| function deviceHref(id, prefix = "") { | |
| return `${prefix}devices/${id}/index.html`; | |
| } | |
| function currentPriceEvent(item) { | |
| return [...item.price_history].sort((a, b) => ( | |
| a.end_year - b.end_year || | |
| a.start_year - b.start_year || | |
| a.catalog_order - b.catalog_order | |
| )).at(-1); | |
| } | |
| function renderStats() { | |
| const host = $("#statStrip"); | |
| if (!host) return; | |
| const stats = [ | |
| [DATA.stats.item_count, "source item pages"], | |
| [DATA.stats.catalog_row_count, "priced catalog rows"], | |
| [`${DATA.stats.max_memory_gb}GB`, "maximum accelerator memory"], | |
| [`${DATA.stats.best_under_5k_memory_gb}GB`, "best current memory under $5k"] | |
| ]; | |
| host.innerHTML = stats.map(([value, label]) => ` | |
| <div class="stat-cell"> | |
| <span class="stat-value">${escapeHTML(value)}</span> | |
| <span class="stat-label">${escapeHTML(label)}</span> | |
| </div> | |
| `).join(""); | |
| } | |
| function setupControls() { | |
| const bandSelect = $("#bandSelect"); | |
| const tableBand = $("#tableBandFilter"); | |
| if (bandSelect) { | |
| bandSelect.innerHTML = DATA.bands.map((band) => ( | |
| `<option value="${band.key}">${escapeHTML(band.label)}</option>` | |
| )).join(""); | |
| bandSelect.value = "3000_5000"; | |
| } | |
| if (tableBand) { | |
| tableBand.innerHTML += DATA.bands.map((band) => ( | |
| `<option value="${band.key}">${escapeHTML(band.label)}</option>` | |
| )).join(""); | |
| } | |
| const producerFilter = $("#producerFilter"); | |
| if (producerFilter) { | |
| producerFilter.innerHTML += producers.map((producer) => ( | |
| `<option value="${producer}">${producer}</option>` | |
| )).join(""); | |
| } | |
| const toggle = $("#producerToggle"); | |
| if (toggle) { | |
| toggle.innerHTML = producers.map((producer) => ( | |
| `<button class="chip" type="button" data-producer="${producer}" aria-pressed="true">${producer}</button>` | |
| )).join(""); | |
| toggle.addEventListener("click", (event) => { | |
| const button = event.target.closest("button[data-producer]"); | |
| if (!button) return; | |
| button.setAttribute("aria-pressed", button.getAttribute("aria-pressed") !== "true"); | |
| renderFrontierChart(); | |
| }); | |
| } | |
| ["metricSelect", "bandSelect"].forEach((id) => { | |
| const el = $(`#${id}`); | |
| if (el) el.addEventListener("change", renderFrontierChart); | |
| }); | |
| ["deviceSearch", "producerFilter", "tableBandFilter"].forEach((id) => { | |
| const el = $(`#${id}`); | |
| if (el) el.addEventListener("input", renderDeviceTable); | |
| }); | |
| } | |
| function activeProducers() { | |
| return [...document.querySelectorAll("#producerToggle button")] | |
| .filter((button) => button.getAttribute("aria-pressed") === "true") | |
| .map((button) => button.dataset.producer); | |
| } | |
| function metricConfig(metric, band) { | |
| if (metric === "projection") { | |
| const labels = { | |
| under_3000: "Under 3k", | |
| "3000_5000": "3k-5k", | |
| "5000_10000": "5k-10k" | |
| }; | |
| const series = []; | |
| for (const [bandKey, label] of Object.entries(labels)) { | |
| const rows = DATA.projection.filter((row) => row.price_band === bandKey); | |
| series.push({ | |
| label: `${label} observed`, | |
| colorKey: `${label} observed`, | |
| dash: false, | |
| points: rows.filter((row) => row.observed_best_so_far_memory_gb !== null).map((row) => ({ | |
| x: row.year, | |
| y: row.observed_best_so_far_memory_gb, | |
| row, | |
| detail: `${label}: observed best-so-far` | |
| })) | |
| }); | |
| series.push({ | |
| label: `${label} linear fit`, | |
| colorKey: `${label} fit`, | |
| dash: true, | |
| points: rows.map((row) => ({ | |
| x: row.year, | |
| y: row.linear_fit_memory_gb, | |
| row, | |
| detail: `${label}: linear scenario` | |
| })) | |
| }); | |
| } | |
| return { | |
| title: "Consumer-band best-so-far memory projection", | |
| subtitle: "Least-squares linear scenario fitted to 2020-2026 best-so-far memory. Not verified future product data.", | |
| yLabel: "Memory capacity", | |
| yFormat: fmtGB, | |
| series | |
| }; | |
| } | |
| const active = activeProducers(); | |
| const bandLabel = DATA.bands.find((b) => b.key === band)?.label || band; | |
| if (metric.startsWith("annual_")) { | |
| const source = DATA.annual.filter((row) => ( | |
| row.price_band === band && row.memory_gb !== null && active.includes(row.producer) | |
| )); | |
| const metricMap = { | |
| annual_memory: ["memory_gb", "Annual memory by producer", "Memory capacity", fmtGB], | |
| annual_price_per_gb: ["price_per_memory_gb", "Annual complete-system USD per GB", "USD per GB", fmtUsdPerGB], | |
| annual_bandwidth: ["bandwidth_gbps", "Annual memory bandwidth", "Bandwidth", fmtBandwidth] | |
| }; | |
| const [field, title, yLabel, yFormat] = metricMap[metric]; | |
| return { | |
| title: `${title} - ${bandLabel}`, | |
| subtitle: "Annual maximum-memory row for each producer in the selected price band.", | |
| yLabel, | |
| yFormat, | |
| series: producers.filter((producer) => active.includes(producer)).map((producer) => ({ | |
| label: producer, | |
| colorKey: producer, | |
| points: source.filter((row) => row.producer === producer && row[field] !== null).map((row) => ({ | |
| x: row.year, | |
| y: row[field], | |
| row, | |
| detail: row.system | |
| })) | |
| })).filter((entry) => entry.points.length) | |
| }; | |
| } | |
| const bestMap = { | |
| best_memory: [DATA.dominant, "memory_gb", "Best-so-far memory", "Memory capacity", fmtGB], | |
| best_price_per_gb: [DATA.best_price, "metric_value", "Best-so-far complete-system USD per GB", "USD per GB", fmtUsdPerGB], | |
| best_bandwidth: [DATA.best_bandwidth, "metric_value", "Best-so-far memory bandwidth", "Bandwidth", fmtBandwidth] | |
| }; | |
| const [rows, field, title, yLabel, yFormat] = bestMap[metric]; | |
| return { | |
| title: `${title} - ${bandLabel}`, | |
| subtitle: "The line carries the previous best forward until a better source-backed point appears.", | |
| yLabel, | |
| yFormat, | |
| series: [{ | |
| label: "Best-so-far", | |
| colorKey: "Best-so-far", | |
| points: rows.filter((row) => row.price_band === band && row[field] !== null).map((row) => ({ | |
| x: row.year, | |
| y: row[field], | |
| row, | |
| detail: row.system | |
| })) | |
| }] | |
| }; | |
| } | |
| function renderFrontierChart() { | |
| const host = $("#frontierChart"); | |
| if (!host) return; | |
| const metric = $("#metricSelect").value; | |
| const band = $("#bandSelect").value; | |
| $("#bandSelect").disabled = metric === "projection"; | |
| const config = metricConfig(metric, band); | |
| renderChart(host, config); | |
| } | |
| function renderChart(host, config) { | |
| const width = 1120; | |
| const height = 560; | |
| const margin = { top: 92, right: 42, bottom: 72, left: 86 }; | |
| const allPoints = config.series.flatMap((series) => series.points); | |
| if (!allPoints.length) { | |
| host.innerHTML = `<div class="empty">No source-backed rows for this selection.</div>`; | |
| return; | |
| } | |
| const xValues = allPoints.map((point) => point.x); | |
| const yValues = allPoints.map((point) => point.y).filter((value) => Number.isFinite(value)); | |
| const xMin = Math.min(...xValues); | |
| const xMax = Math.max(...xValues); | |
| const yMin = Math.min(0, Math.min(...yValues)); | |
| const yMax = Math.max(...yValues) * 1.16; | |
| const plotW = width - margin.left - margin.right; | |
| const plotH = height - margin.top - margin.bottom; | |
| const xScale = (x) => margin.left + ((x - xMin) / Math.max(1, xMax - xMin)) * plotW; | |
| const yScale = (y) => margin.top + plotH - ((y - yMin) / Math.max(1, yMax - yMin)) * plotH; | |
| const years = []; | |
| for (let year = xMin; year <= xMax; year += 1) years.push(year); | |
| const tickCount = 5; | |
| const yTicks = Array.from({ length: tickCount }, (_, index) => ( | |
| yMin + ((yMax - yMin) / (tickCount - 1)) * index | |
| )); | |
| const linePath = (points) => points | |
| .filter((point) => Number.isFinite(point.y)) | |
| .map((point, index) => `${index === 0 ? "M" : "L"} ${xScale(point.x).toFixed(2)} ${yScale(point.y).toFixed(2)}`) | |
| .join(" "); | |
| const seriesSvg = config.series.map((series) => { | |
| const color = producerColors[series.colorKey] || "var(--accent)"; | |
| const dash = series.dash ? ' stroke-dasharray="8 8"' : ""; | |
| const path = linePath(series.points); | |
| const points = series.points.map((point) => { | |
| const itemId = point.row?.item_id || ""; | |
| return `<circle class="chart-point" data-item="${escapeHTML(itemId)}" data-label="${escapeHTML(series.label)}" data-detail="${escapeHTML(point.detail || "")}" data-x="${point.x}" data-y="${point.y}" cx="${xScale(point.x).toFixed(2)}" cy="${yScale(point.y).toFixed(2)}" r="5.8" fill="${color}" stroke="var(--bg)" stroke-width="1.5"></circle>`; | |
| }).join(""); | |
| return `<path d="${path}" fill="none" stroke="${color}" stroke-width="3"${dash}></path>${points}`; | |
| }).join(""); | |
| const legend = config.series.map((series, index) => { | |
| const color = producerColors[series.colorKey] || "var(--accent)"; | |
| const x = margin.left + (index % 3) * 260; | |
| const y = 58 + Math.floor(index / 3) * 24; | |
| return `<g transform="translate(${x} ${y})"><line x1="0" x2="24" y1="0" y2="0" stroke="${color}" stroke-width="3"${series.dash ? ' stroke-dasharray="6 6"' : ""}></line><text x="34" y="4" fill="var(--muted)" font-size="13">${escapeHTML(series.label)}</text></g>`; | |
| }).join(""); | |
| host.innerHTML = ` | |
| <div class="chart-title"> | |
| <h3>${escapeHTML(config.title)}</h3> | |
| <p class="chart-subtitle">${escapeHTML(config.subtitle)}</p> | |
| </div> | |
| <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHTML(config.title)}"> | |
| <g class="grid"> | |
| ${yTicks.map((tick) => `<line x1="${margin.left}" x2="${width - margin.right}" y1="${yScale(tick).toFixed(2)}" y2="${yScale(tick).toFixed(2)}" stroke="var(--line)" stroke-width="1"></line>`).join("")} | |
| </g> | |
| <g class="axes"> | |
| <line x1="${margin.left}" x2="${width - margin.right}" y1="${height - margin.bottom}" y2="${height - margin.bottom}" stroke="var(--line-strong)"></line> | |
| <line x1="${margin.left}" x2="${margin.left}" y1="${margin.top}" y2="${height - margin.bottom}" stroke="var(--line-strong)"></line> | |
| </g> | |
| <g class="ticks"> | |
| ${years.map((year) => `<text x="${xScale(year).toFixed(2)}" y="${height - margin.bottom + 28}" text-anchor="middle" fill="var(--muted)" font-size="13">${year}</text>`).join("")} | |
| ${yTicks.map((tick) => `<text x="${margin.left - 14}" y="${yScale(tick).toFixed(2) + 4}" text-anchor="end" fill="var(--muted)" font-size="13">${escapeHTML(config.yFormat(tick))}</text>`).join("")} | |
| </g> | |
| <text x="${margin.left}" y="${height - 24}" fill="var(--faint)" font-size="13">Year</text> | |
| <text x="24" y="${margin.top}" fill="var(--faint)" font-size="13" transform="rotate(-90 24 ${margin.top})">${escapeHTML(config.yLabel)}</text> | |
| <g class="series">${seriesSvg}</g> | |
| <g class="legend">${legend}</g> | |
| </svg> | |
| <div class="tooltip" hidden></div> | |
| `; | |
| const tooltip = $(".tooltip", host); | |
| host.querySelectorAll(".chart-point").forEach((point) => { | |
| point.addEventListener("mouseenter", () => { | |
| const item = byId.get(point.dataset.item); | |
| tooltip.hidden = false; | |
| tooltip.innerHTML = ` | |
| <strong>${escapeHTML(point.dataset.label)}: ${escapeHTML(config.yFormat(point.dataset.y))}</strong> | |
| <span>${escapeHTML(point.dataset.x)} ${point.dataset.detail ? "- " + escapeHTML(point.dataset.detail) : ""}</span> | |
| ${item ? `<span>${escapeHTML(item.name)}</span><span>Click to open item page.</span>` : ""} | |
| `; | |
| const box = point.getBoundingClientRect(); | |
| const hostBox = host.getBoundingClientRect(); | |
| tooltip.style.left = `${box.left - hostBox.left + box.width / 2}px`; | |
| tooltip.style.top = `${box.top - hostBox.top}px`; | |
| }); | |
| point.addEventListener("mouseleave", () => { | |
| tooltip.hidden = true; | |
| }); | |
| point.addEventListener("click", () => { | |
| if (point.dataset.item) { | |
| window.location.href = deviceHref(point.dataset.item); | |
| } | |
| }); | |
| }); | |
| } | |
| function renderDeviceTable() { | |
| const host = $("#deviceTable"); | |
| if (!host) return; | |
| const search = ($("#deviceSearch")?.value || "").trim().toLowerCase(); | |
| const producer = $("#producerFilter")?.value || "all"; | |
| const band = $("#tableBandFilter")?.value || "all"; | |
| const rows = DATA.items.filter((item) => { | |
| const haystack = [ | |
| item.name, | |
| item.producer, | |
| item.memory.type, | |
| item.accelerators.map((accel) => accel.name).join(" ") | |
| ].join(" ").toLowerCase(); | |
| return (!search || haystack.includes(search)) | |
| && (producer === "all" || item.producer === producer) | |
| && (band === "all" || item.latest_price_band === band); | |
| }); | |
| if (!rows.length) { | |
| host.innerHTML = `<div class="empty">No devices match the current filters.</div>`; | |
| return; | |
| } | |
| host.innerHTML = ` | |
| <table class="device-table"> | |
| <thead> | |
| <tr> | |
| <th>Device</th> | |
| <th>Producer</th> | |
| <th>Memory</th> | |
| <th>Bandwidth</th> | |
| <th>Latest price</th> | |
| <th>Price band</th> | |
| <th>Evidence</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${rows.map((item) => { | |
| const event = currentPriceEvent(item); | |
| return `<tr> | |
| <td><a class="device-link" href="${deviceHref(item.id)}"><strong>${escapeHTML(item.name)}</strong></a><br><span class="pill">${escapeHTML(item.item_kind.replaceAll("_", " "))}</span></td> | |
| <td><span class="producer-${item.producer}">${item.producer}</span></td> | |
| <td>${fmtGB(item.memory.capacity_gb)} <span class="pill">${escapeHTML(item.memory.symbol)}</span><br>${escapeHTML(item.memory.type)}</td> | |
| <td>${fmtBandwidth(item.memory.bandwidth_gbps)}</td> | |
| <td>${fmtMoney(event.price_usd)}<br><span class="pill">${escapeHTML(event.price_kind.replaceAll("_", " "))}</span></td> | |
| <td>${escapeHTML(DATA.bands.find((entry) => entry.key === event.price_band)?.label || event.price_band)}</td> | |
| <td>${escapeHTML(event.evidence)}<br><span class="pill">${escapeHTML(event.confidence)}</span></td> | |
| </tr>`; | |
| }).join("")} | |
| </tbody> | |
| </table> | |
| `; | |
| } | |
| function renderDevicePage() { | |
| const id = window.LOCAL_FRONTIER_DEVICE_ID; | |
| const item = byId.get(id); | |
| if (!item) return; | |
| const event = currentPriceEvent(item); | |
| const hero = $("#deviceHero"); | |
| if (hero) { | |
| hero.innerHTML = [ | |
| [`${item.memory.capacity_gb}GB`, `${item.memory.symbol} ${item.memory.type}`], | |
| [fmtBandwidth(item.memory.bandwidth_gbps), "source-backed peak memory bandwidth"], | |
| [fmtMoney(event.price_usd), `${event.end_year} ${event.price_kind.replaceAll("_", " ")}`], | |
| [DATA.bands.find((entry) => entry.key === event.price_band)?.label || event.price_band, "complete-system price band"] | |
| ].map(([value, label]) => ` | |
| <div class="stat-cell"> | |
| <span class="stat-value">${escapeHTML(value)}</span> | |
| <span class="stat-label">${escapeHTML(label)}</span> | |
| </div> | |
| `).join(""); | |
| } | |
| const accelerators = item.accelerators.length | |
| ? item.accelerators.map((accel) => `${accel.count ? `${accel.count}x ` : ""}${accel.vendor} ${accel.name}`).join("<br>") | |
| : "Not specified separately"; | |
| const frontierRows = [ | |
| ...DATA.annual.filter((row) => row.item_id === item.id), | |
| ...DATA.dominant.filter((row) => row.item_id === item.id) | |
| ]; | |
| const host = $("#devicePage"); | |
| host.innerHTML = ` | |
| <section class="detail-block"> | |
| <div class="detail-card"> | |
| <h2>Specification</h2> | |
| <dl class="detail-list"> | |
| <div><dt>Producer</dt><dd><span class="producer-${item.producer}">${item.producer}</span></dd></div> | |
| <div><dt>Memory</dt><dd>${fmtGB(item.memory.capacity_gb)} ${escapeHTML(item.memory.symbol)} - ${escapeHTML(item.memory.type)}</dd></div> | |
| <div><dt>Bandwidth</dt><dd>${fmtBandwidth(item.memory.bandwidth_gbps)}</dd></div> | |
| <div><dt>Accelerators</dt><dd>${accelerators}</dd></div> | |
| <div><dt>Kind</dt><dd>${escapeHTML(item.item_kind.replaceAll("_", " "))}</dd></div> | |
| </dl> | |
| </div> | |
| <div class="detail-card"> | |
| <h2>Price History</h2> | |
| <div class="table-wrap">${priceHistoryTable(item)}</div> | |
| </div> | |
| <div class="detail-card"> | |
| <h2>Catalog Rows</h2> | |
| <div class="table-wrap">${catalogRowsTable(item.catalog_rows)}</div> | |
| </div> | |
| </section> | |
| <aside class="detail-block"> | |
| <div class="detail-card"> | |
| <h2>Sources</h2> | |
| ${sourceList(item)} | |
| </div> | |
| <div class="detail-card"> | |
| <h2>Frontier Appearances</h2> | |
| ${frontierRows.length ? frontierRowsTable(frontierRows) : `<p class="empty">This item is in the catalog but does not become a selected annual or best-so-far frontier row.</p>`} | |
| </div> | |
| <div class="detail-card"> | |
| <h2>Notes</h2> | |
| <p class="prose">${escapeHTML(event.note || "No additional note.")}</p> | |
| ${item.memory.bandwidth_note ? `<p class="prose">${escapeHTML(item.memory.bandwidth_note)}</p>` : ""} | |
| </div> | |
| </aside> | |
| `; | |
| } | |
| function priceHistoryTable(item) { | |
| return `<table><thead><tr><th>Years</th><th>Price</th><th>Band</th><th>Evidence</th></tr></thead><tbody> | |
| ${item.price_history.map((event) => `<tr> | |
| <td>${event.start_year}-${event.end_year}</td> | |
| <td>${fmtMoney(event.price_usd)}</td> | |
| <td>${escapeHTML(DATA.bands.find((entry) => entry.key === event.price_band)?.label || event.price_band)}</td> | |
| <td>${escapeHTML(event.evidence)}<br><span class="pill">${escapeHTML(event.confidence)}</span></td> | |
| </tr>`).join("")} | |
| </tbody></table>`; | |
| } | |
| function catalogRowsTable(rows) { | |
| if (!rows.length) return `<div class="empty">No generated catalog rows.</div>`; | |
| return `<table><thead><tr><th>Years</th><th>Price</th><th>Band</th><th>Price/GB</th></tr></thead><tbody> | |
| ${rows.map((row) => `<tr> | |
| <td>${row.start_year}-${row.end_year}</td> | |
| <td>${fmtMoney(row.price_usd)}</td> | |
| <td>${escapeHTML(DATA.bands.find((entry) => entry.key === row.price_band)?.label || row.price_band)}</td> | |
| <td>${fmtUsdPerGB(row.price_per_memory_gb)}</td> | |
| </tr>`).join("")} | |
| </tbody></table>`; | |
| } | |
| function frontierRowsTable(rows) { | |
| return `<div class="table-wrap"><table><thead><tr><th>Year</th><th>Band</th><th>Metric</th><th>Value</th></tr></thead><tbody> | |
| ${rows.map((row) => `<tr> | |
| <td>${row.year}</td> | |
| <td>${escapeHTML(DATA.bands.find((entry) => entry.key === row.price_band)?.label || row.price_band)}</td> | |
| <td>${row.metric_name ? escapeHTML(row.metric_name.replaceAll("_", " ")) : "memory frontier"}</td> | |
| <td>${row.metric_value !== undefined ? escapeHTML(String(row.metric_value)) : fmtGB(row.memory_gb)}</td> | |
| </tr>`).join("")} | |
| </tbody></table></div>`; | |
| } | |
| function sourceList(item) { | |
| const sources = []; | |
| for (const source of item.spec_sources || []) { | |
| sources.push({ name: source.name, url: source.url }); | |
| } | |
| if (item.memory.bandwidth_source_url) { | |
| sources.push({ name: "Memory bandwidth source", url: item.memory.bandwidth_source_url }); | |
| } | |
| for (const event of item.price_history) { | |
| sources.push({ name: `${event.start_year}-${event.end_year} price source`, url: event.source_url }); | |
| } | |
| const unique = []; | |
| const seen = new Set(); | |
| for (const source of sources) { | |
| if (!source.url || seen.has(source.url)) continue; | |
| seen.add(source.url); | |
| unique.push(source); | |
| } | |
| if (!unique.length) return `<p class="empty">No source links recorded.</p>`; | |
| return `<ul class="source-list">${unique.map((source) => `<li><a href="${escapeHTML(source.url)}" rel="noreferrer">${escapeHTML(source.name || source.url)}</a></li>`).join("")}</ul>`; | |
| } | |
| function init() { | |
| if (document.body.dataset.page === "device") { | |
| renderDevicePage(); | |
| return; | |
| } | |
| renderStats(); | |
| setupControls(); | |
| renderFrontierChart(); | |
| renderDeviceTable(); | |
| } | |
| init(); | |
| """ | |
| def space_readme() -> str: | |
| return """--- | |
| title: Local Frontier | |
| sdk: static | |
| app_file: index.html | |
| license: mit | |
| short_description: Interactive local AI hardware frontier database. | |
| --- | |
| # Local Frontier | |
| Interactive static database for the local hardware frontier notes. Generated from | |
| the item JSON and CSV artifacts in `docs/local_hardware_frontier/`. | |
| """ | |
| def build() -> None: | |
| payload = build_payload() | |
| if SPACE_DIR.exists(): | |
| shutil.rmtree(SPACE_DIR) | |
| ASSETS_DIR.mkdir(parents=True) | |
| DEVICES_DIR.mkdir(parents=True) | |
| write(SPACE_DIR / "README.md", space_readme()) | |
| write(ASSETS_DIR / "app.css", CSS) | |
| write(ASSETS_DIR / "app.js", JS) | |
| write(ASSETS_DIR / "local-frontier-data.js", json_script(payload)) | |
| write( | |
| SPACE_DIR / "index.html", | |
| page_shell( | |
| title="Local Frontier", | |
| description="Interactive local AI hardware frontier database.", | |
| body=index_body(), | |
| ), | |
| ) | |
| for item in payload["items"]: | |
| write( | |
| DEVICES_DIR / item["id"] / "index.html", | |
| page_shell( | |
| title=f"{item['name']} - Local Frontier", | |
| description=f"{item['producer']} local accelerator memory item page.", | |
| body=device_body(item), | |
| asset_prefix="../../", | |
| page="device", | |
| device_id=item["id"], | |
| ), | |
| ) | |
| print(f"Built static Space at {SPACE_DIR}") | |
| print(f"Device pages: {len(payload['items'])}") | |
| if __name__ == "__main__": | |
| build() | |
Xet Storage Details
- Size:
- 47.9 kB
- Xet hash:
- 6e8dff268250fe2ef12730d77a69456e6840fbcb412214162fac063a855fb400
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.