lerobot/robot-learning-tutorial
508
1<script>2 import * as d3 from "d3";3 import { formatAbbrev, smoothMetricData } from "./core/chart-utils.js";4 import {5 generateRunNames,6 genCurves,7 Random,8 Performance,9 generateMassiveTestDataset,10 } from "./core/data-generator.js";11 import Legend from "./components/Legend.svelte";12 import Cell from "./components/Cell.svelte";13 import FullscreenModal from "./components/FullscreenModal.svelte";14 import { onMount, onDestroy } from "svelte";15 import { jitterTrigger } from "./core/store.js";16 17 export let variant = "classic"; // 'classic' | 'oblivion'18 export let normalizeLoss = true;19 export let logScaleX = false;20 export let smoothing = false;21 22 let hostEl;23 let gridEl;24 let legendItems = [];25 const cellsDef = [26 { metric: "epoch", title: "Epoch" },27 { metric: "train_accuracy", title: "Train accuracy" },28 { metric: "train_loss", title: "Train loss" },29 { metric: "val_accuracy", title: "Val accuracy" },30 { metric: "val_loss", title: "Val loss", wide: true },31 ];32 let preparedData = {};33 let colorsByRun = {};34 35 // Variables for data management (will be initialized in onMount)36 let dataByMetric = new Map();37 let metricsToDraw = [];38 let currentRunList = [];39 let cycleIdx = 2;40 41 // Dynamic color palette using color-palettes.js helper42 let dynamicPalette = [43 "#0ea5e9",44 "#8b5cf6",45 "#f59e0b",46 "#ef4444",47 "#10b981",48 "#f97316",49 "#3b82f6",50 "#8b5ad6",51 ]; // fallback52 53 const updateDynamicPalette = () => {54 if (55 typeof window !== "undefined" &&56 window.ColorPalettes &&57 currentRunList.length > 058 ) {59 try {60 dynamicPalette = window.ColorPalettes.getColors(61 "categorical",62 currentRunList.length,63 );64 } catch (e) {65 console.warn("Failed to generate dynamic palette:", e);66 // Keep fallback palette67 }68 }69 };70 71 const colorForRun = (name) => {72 const idx = currentRunList.indexOf(name);73 return idx >= 0 ? dynamicPalette[idx % dynamicPalette.length] : "#999";74 };75 76 // Jitter function - generates completely new data with new runs77 function jitterData() {78 console.log(79 "jitterData called - generating new data with random number of runs",80 ); // Debug log81 82 // Generate new random data with weighted probability for fewer runs83 // Higher probability for 2-3 runs, lower for 4-5-6 runs84 const rand = Math.random();85 let wantRuns;86 if (rand < 0.4)87 wantRuns = 2; // 40% chance88 else if (rand < 0.7)89 wantRuns = 3; // 30% chance90 else if (rand < 0.85)91 wantRuns = 4; // 15% chance92 else if (rand < 0.95)93 wantRuns = 5; // 10% chance94 else wantRuns = 6; // 5% chance95 // Use realistic ML training step counts96 const stepsCount = Random.trainingSteps();97 const runsSim = generateRunNames(wantRuns, stepsCount);98 const steps = Array.from({ length: stepsCount }, (_, i) => i + 1);99 const nextByMetric = new Map();100 const TARGET_METRICS = [101 "epoch",102 "train_accuracy",103 "train_loss",104 "val_accuracy",105 "val_loss",106 ];107 108 // Initialize data structure109 TARGET_METRICS.forEach((tgt) => {110 const map = {};111 runsSim.forEach((r) => {112 map[r] = [];113 });114 nextByMetric.set(tgt, map);115 });116 117 // Generate curves for each run118 runsSim.forEach((run) => {119 const curves = genCurves(stepsCount);120 steps.forEach((s, i) => {121 nextByMetric.get("epoch")[run].push({ step: s, value: s });122 nextByMetric123 .get("train_accuracy")124 [run].push({ step: s, value: curves.accTrain[i] });125 nextByMetric126 .get("val_accuracy")127 [run].push({ step: s, value: curves.accVal[i] });128 nextByMetric129 .get("train_loss")130 [run].push({ step: s, value: curves.lossTrain[i] });131 nextByMetric132 .get("val_loss")133 [run].push({ step: s, value: curves.lossVal[i] });134 });135 });136 137 // Update all reactive data138 nextByMetric.forEach((v, k) => dataByMetric.set(k, v));139 metricsToDraw = TARGET_METRICS;140 currentRunList = runsSim.slice();141 updateDynamicPalette(); // Generate new colors based on run count142 legendItems = currentRunList.map((name) => ({143 name,144 color: colorForRun(name),145 }));146 updatePreparedData();147 colorsByRun = Object.fromEntries(148 currentRunList.map((name) => [name, colorForRun(name)]),149 );150 151 console.log(152 `jitterData completed - generated ${wantRuns} runs with ${stepsCount} steps`,153 ); // Debug log154 }155 156 // Public API: allow external theme switch157 function setTheme(name) {158 variant = name === "oblivion" ? "oblivion" : "classic";159 updateThemeClass();160 161 // Debug log for font application162 if (typeof window !== "undefined") {163 console.log(`Theme switched to: ${variant}`);164 if (hostEl) {165 const computedStyle = getComputedStyle(hostEl);166 const appliedFont = computedStyle.fontFamily;167 console.log(`Applied font-family: ${appliedFont}`);168 }169 }170 }171 172 // Public API: allow external log scale X toggle173 function setLogScaleX(enabled) {174 logScaleX = enabled;175 console.log(`Log scale X set to: ${logScaleX}`);176 }177 178 // Public API: allow external smoothing toggle179 function setSmoothing(enabled) {180 smoothing = enabled;181 console.log(`Smoothing set to: ${smoothing}`);182 // Re-prepare data with smoothing applied183 updatePreparedData();184 }185 186 // Public API: generate massive test dataset187 function generateMassiveDataset(steps = null, runs = 3) {188 console.log(189 "๐งช Generating massive test dataset for sampling validation...",190 );191 192 const result = generateMassiveTestDataset(steps, runs);193 194 // Update reactive data with massive dataset195 result.dataByMetric.forEach((v, k) => dataByMetric.set(k, v));196 metricsToDraw = [197 "epoch",198 "train_accuracy",199 "train_loss",200 "val_accuracy",201 "val_loss",202 ];203 currentRunList = result.runNames.slice();204 updateDynamicPalette();205 legendItems = currentRunList.map((name) => ({206 name,207 color: colorForRun(name),208 }));209 updatePreparedData();210 colorsByRun = Object.fromEntries(211 currentRunList.map((name) => [name, colorForRun(name)]),212 );213 214 console.log(215 `โ
Massive dataset loaded: ${result.stepCount} steps ร ${result.runNames.length} runs`,216 );217 console.log(`๐ Total data points: ${result.totalPoints.toLocaleString()}`);218 console.log(`๐ฏ Description: ${result.description}`);219 220 return result;221 }222 223 // Public API: add live data point for simulation224 function addLiveDataPoint(runName, dataPoint) {225 console.log(`Adding live data point for run "${runName}":`, dataPoint);226 227 // Add run to currentRunList if it doesn't exist228 if (!currentRunList.includes(runName)) {229 currentRunList = [...currentRunList, runName];230 updateDynamicPalette();231 colorsByRun = Object.fromEntries(232 currentRunList.map((name) => [name, colorForRun(name)]),233 );234 legendItems = currentRunList.map((name) => ({235 name,236 color: colorForRun(name),237 }));238 }239 240 // Initialize data structures for the run if needed241 const TARGET_METRICS = [242 "epoch",243 "train_accuracy",244 "train_loss",245 "val_accuracy",246 "val_loss",247 ];248 TARGET_METRICS.forEach((metric) => {249 if (!dataByMetric.has(metric)) {250 dataByMetric.set(metric, {});251 }252 const metricData = dataByMetric.get(metric);253 if (!metricData[runName]) {254 metricData[runName] = [];255 }256 });257 258 // Add the new data points to each metric259 const step = dataPoint.step;260 261 // Add epoch data262 const epochData = dataByMetric.get("epoch");263 epochData[runName].push({ step, value: step });264 265 // Add accuracy data (train and val get the same value for simplicity)266 if (dataPoint.accuracy !== undefined) {267 const trainAccData = dataByMetric.get("train_accuracy");268 const valAccData = dataByMetric.get("val_accuracy");269 270 // Add some noise between train and val accuracy271 const trainAcc = dataPoint.accuracy;272 const valAcc = Math.max(273 0,274 Math.min(1, dataPoint.accuracy - 0.01 - Math.random() * 0.03),275 );276 277 trainAccData[runName].push({ step, value: trainAcc });278 valAccData[runName].push({ step, value: valAcc });279 }280 281 // Add loss data (train and val get the same value for simplicity)282 if (dataPoint.loss !== undefined) {283 const trainLossData = dataByMetric.get("train_loss");284 const valLossData = dataByMetric.get("val_loss");285 286 // Add some noise between train and val loss287 const trainLoss = dataPoint.loss;288 const valLoss = dataPoint.loss + 0.05 + Math.random() * 0.1;289 290 trainLossData[runName].push({ step, value: trainLoss });291 valLossData[runName].push({ step, value: valLoss });292 }293 294 // Update all metrics to draw295 metricsToDraw = TARGET_METRICS;296 297 // Update prepared data with new values298 updatePreparedData();299 300 console.log(301 `Live data point added successfully. Total runs: ${currentRunList.length}`,302 );303 }304 305 // Update prepared data with optional smoothing306 let preparedRawData = {}; // Store original data for background display307 308 function updatePreparedData() {309 const TARGET_METRICS = [310 "epoch",311 "train_accuracy",312 "train_loss",313 "val_accuracy",314 "val_loss",315 ];316 let dataToUse = {};317 let rawDataToStore = {};318 319 TARGET_METRICS.forEach((metric) => {320 const rawData = dataByMetric.get(metric);321 if (rawData) {322 // Store original data323 rawDataToStore[metric] = rawData;324 325 // Apply smoothing if enabled (except for epoch which should stay exact)326 dataToUse[metric] =327 smoothing && metric !== "epoch"328 ? smoothMetricData(rawData, 5) // Window size of 5329 : rawData;330 }331 });332 333 preparedData = dataToUse;334 preparedRawData = rawDataToStore;335 console.log(`Prepared data updated, smoothing: ${smoothing}`);336 }337 338 function updateThemeClass() {339 if (!hostEl) return;340 hostEl.classList.toggle("theme--classic", variant === "classic");341 hostEl.classList.toggle("theme--oblivion", variant === "oblivion");342 hostEl.setAttribute("data-variant", variant);343 }344 345 $: updateThemeClass();346 347 // Chart logic now handled by Cell.svelte348 349 // Fullscreen navigation state350 let currentFullscreenIndex = 0;351 let isModalOpen = false;352 353 function handleNavigate(newIndex) {354 currentFullscreenIndex = newIndex;355 }356 357 function openModal(index) {358 currentFullscreenIndex = index;359 isModalOpen = true;360 }361 362 function closeModal() {363 isModalOpen = false;364 }365 366 // Prepare all charts data for navigation367 $: allChartsData = cellsDef.map((c) => ({368 metricKey: c.metric,369 titleText: c.title,370 metricData: (preparedData && preparedData[c.metric]) || {},371 rawMetricData: (preparedRawData && preparedRawData[c.metric]) || {},372 }));373 374 // Color function for the modal375 $: modalColorForRun = (name) => colorsByRun[name] || "#999";376 377 let cleanup = null;378 onMount(() => {379 if (!hostEl || !gridEl) return;380 hostEl.__setTheme = setTheme;381 382 // Jitter & Simulate functions383 function rebuildLegend() {384 updateDynamicPalette(); // Update colors when adding new data385 legendItems = currentRunList.map((name) => ({386 name,387 color: colorForRun(name),388 }));389 }390 391 function simulateData() {392 // Generate new random data with weighted probability for fewer runs393 // Higher probability for 2-3 runs, lower for 4-5-6 runs394 const rand = Math.random();395 let wantRuns;396 if (rand < 0.4)397 wantRuns = 2; // 40% chance398 else if (rand < 0.7)399 wantRuns = 3; // 30% chance400 else if (rand < 0.85)401 wantRuns = 4; // 15% chance402 else if (rand < 0.95)403 wantRuns = 5; // 10% chance404 else wantRuns = 6; // 5% chance405 // Use realistic ML training step counts with cycling scenarios406 let stepsCount;407 if (cycleIdx === 0) {408 stepsCount = Random.trainingStepsForScenario("prototyping");409 } else if (cycleIdx === 1) {410 stepsCount = Random.trainingStepsForScenario("development");411 } else if (cycleIdx === 2) {412 stepsCount = Random.trainingStepsForScenario("production");413 } else if (cycleIdx === 3) {414 stepsCount = Random.trainingStepsForScenario("research");415 } else if (cycleIdx === 4) {416 stepsCount = Random.trainingStepsForScenario("llm");417 } else if (cycleIdx === 5) {418 stepsCount = Random.trainingStepsForScenario("massive");419 } else {420 stepsCount = Random.trainingSteps(); // Full range for variety421 }422 cycleIdx = (cycleIdx + 1) % 7; // Cycle through 7 scenarios now423 424 const runsSim = generateRunNames(wantRuns, stepsCount);425 const steps = Array.from({ length: stepsCount }, (_, i) => i + 1);426 const nextByMetric = new Map();427 const TARGET_METRICS = [428 "epoch",429 "train_accuracy",430 "train_loss",431 "val_accuracy",432 "val_loss",433 ];434 const mList =435 metricsToDraw && metricsToDraw.length ? metricsToDraw : TARGET_METRICS;436 mList.forEach((tgt) => {437 const map = {};438 runsSim.forEach((r) => {439 map[r] = [];440 });441 nextByMetric.set(tgt, map);442 });443 runsSim.forEach((run) => {444 const curves = genCurves(stepsCount);445 steps.forEach((s, i) => {446 if (mList.includes("epoch"))447 nextByMetric.get("epoch")[run].push({ step: s, value: s });448 if (mList.includes("train_accuracy"))449 nextByMetric450 .get("train_accuracy")451 [run].push({ step: s, value: curves.accTrain[i] });452 if (mList.includes("val_accuracy"))453 nextByMetric454 .get("val_accuracy")455 [run].push({ step: s, value: curves.accVal[i] });456 if (mList.includes("train_loss"))457 nextByMetric458 .get("train_loss")459 [run].push({ step: s, value: curves.lossTrain[i] });460 if (mList.includes("val_loss"))461 nextByMetric462 .get("val_loss")463 [run].push({ step: s, value: curves.lossVal[i] });464 });465 });466 nextByMetric.forEach((v, k) => dataByMetric.set(k, v));467 currentRunList = runsSim.slice();468 rebuildLegend();469 updatePreparedData();470 updateDynamicPalette(); // Update colors when rebuilding471 colorsByRun = Object.fromEntries(472 currentRunList.map((name) => [name, colorForRun(name)]),473 );474 }475 // No need for event listeners anymore - we'll use reactive statement476 477 // Start with level 3 long synthetic data for consistency478 simulateData();479 // Svelte Cells will react to preparedData/colorsByRun updates480 481 cleanup = () => {482 // No cleanup needed for reactive statements483 };484 });485 486 onDestroy(() => {487 if (cleanup) cleanup();488 });489 490 // Expose instance for debugging and external theme control491 onMount(() => {492 window.trackioInstance = {493 jitterData,494 addLiveDataPoint,495 generateMassiveDataset,496 };497 if (hostEl) {498 hostEl.__trackioInstance = {499 setTheme,500 setLogScaleX,501 setSmoothing,502 jitterData,503 addLiveDataPoint,504 generateMassiveDataset,505 };506 }507 508 // Initialize dynamic palette509 updateDynamicPalette();510 511 // Listen for palette updates from color-palettes.js512 const handlePaletteUpdate = () => {513 updateDynamicPalette();514 // Rebuild legend and colors if needed515 if (currentRunList.length > 0) {516 legendItems = currentRunList.map((name) => ({517 name,518 color: colorForRun(name),519 }));520 colorsByRun = Object.fromEntries(521 currentRunList.map((name) => [name, colorForRun(name)]),522 );523 }524 };525 526 document.addEventListener("palettes:updated", handlePaletteUpdate);527 528 // Cleanup listener on destroy529 return () => {530 document.removeEventListener("palettes:updated", handlePaletteUpdate);531 };532 });533 534 // React to jitter trigger from store535 $: {536 console.log(537 "Reactive statement triggered, jitterTrigger value:",538 $jitterTrigger,539 );540 if ($jitterTrigger > 0) {541 console.log(542 "Jitter trigger activated:",543 $jitterTrigger,544 "calling jitterData()",545 );546 jitterData();547 }548 }549 550 // Legend ghost helpers (hover effects)551 function ghostRun(run) {552 try {553 hostEl.classList.add("hovering");554 555 // Ghost the chart lines and points556 hostEl.querySelectorAll(".cell").forEach((cell) => {557 cell558 .querySelectorAll("svg .lines path.run-line")559 .forEach((p) =>560 p.classList.toggle("ghost", p.getAttribute("data-run") !== run),561 );562 cell563 .querySelectorAll("svg .lines path.raw-line")564 .forEach((p) =>565 p.classList.toggle("ghost", p.getAttribute("data-run") !== run),566 );567 cell568 .querySelectorAll("svg .points circle.pt")569 .forEach((c) =>570 c.classList.toggle("ghost", c.getAttribute("data-run") !== run),571 );572 });573 574 // Ghost the legend items575 hostEl.querySelectorAll(".legend-bottom .item").forEach((item) => {576 const itemRun = item.getAttribute("data-run");577 item.classList.toggle("ghost", itemRun !== run);578 });579 } catch (_) {}580 }581 function clearGhost() {582 try {583 hostEl.classList.remove("hovering");584 585 // Clear ghost from chart lines and points586 hostEl.querySelectorAll(".cell").forEach((cell) => {587 cell588 .querySelectorAll("svg .lines path.run-line")589 .forEach((p) => p.classList.remove("ghost"));590 cell591 .querySelectorAll("svg .lines path.raw-line")592 .forEach((p) => p.classList.remove("ghost"));593 cell594 .querySelectorAll("svg .points circle.pt")595 .forEach((c) => c.classList.remove("ghost"));596 });597 598 // Clear ghost from legend items599 hostEl.querySelectorAll(".legend-bottom .item").forEach((item) => {600 item.classList.remove("ghost");601 });602 } catch (_) {}603 }604</script>605 606<div class="trackio theme--classic" bind:this={hostEl} data-variant={variant}>607 <div class="trackio__header">608 <Legend609 items={legendItems}610 on:legend-hover={(e) => {611 const run = e?.detail?.name;612 if (!run) return;613 ghostRun(run);614 }}615 on:legend-leave={() => {616 clearGhost();617 }}618 />619 </div>620 <div class="trackio__grid" bind:this={gridEl}>621 {#each cellsDef as c, i}622 <Cell623 metricKey={c.metric}624 titleText={c.title}625 wide={c.wide}626 {variant}627 {normalizeLoss}628 {logScaleX}629 {smoothing}630 metricData={(preparedData && preparedData[c.metric]) || {}}631 rawMetricData={(preparedRawData && preparedRawData[c.metric]) || {}}632 colorForRun={(name) => colorsByRun[name] || "#999"}633 {hostEl}634 currentIndex={i}635 onOpenModal={openModal}636 />637 {/each}638 </div>639 <div class="trackio__footer">640 <small>641 Built with <a642 href="https://github.com/huggingface/trackio"643 target="_blank"644 rel="noopener noreferrer">TrackIO</a645 >646 <span class="separator">โข</span>647 <a648 href="https://huggingface.co/docs/hub/spaces-sdks-docker"649 target="_blank"650 rel="noopener noreferrer">Use via API</a651 >652 </small>653 </div>654</div>655 656<!-- Centralized Fullscreen Modal -->657<FullscreenModal658 visible={isModalOpen}659 title={allChartsData[currentFullscreenIndex]?.titleText || ""}660 metricData={allChartsData[currentFullscreenIndex]?.metricData || {}}661 rawMetricData={allChartsData[currentFullscreenIndex]?.rawMetricData || {}}662 colorForRun={modalColorForRun}663 {variant}664 {logScaleX}665 {smoothing}666 {normalizeLoss}667 metricKey={allChartsData[currentFullscreenIndex]?.metricKey || ""}668 titleText={allChartsData[currentFullscreenIndex]?.titleText || ""}669 currentIndex={currentFullscreenIndex}670 totalCharts={cellsDef.length}671 onNavigate={handleNavigate}672 on:close={closeModal}673/>674 675<style>676 /* =========================677 TRACKIO THEME SYSTEM678 ========================= */679 680 /* Font imports for themes - ensure Roboto Mono is loaded for Oblivion theme */681 @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap");682 683 /* Fallback font-face declaration */684 @font-face {685 font-family: "Roboto Mono Fallback";686 src: url("https://fonts.gstatic.com/s/robotomono/v23/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW4AJi8SJQt.woff2")687 format("woff2");688 font-weight: 400;689 font-style: normal;690 font-display: swap;691 }692 693 /* Base variables - all themes inherit these */694 .trackio {695 position: relative;696 --z-tooltip: 50;697 --z-overlay: 99999999;698 699 /* Typography */700 --trackio-font-family: var(701 --font-mono,702 ui-monospace,703 SFMono-Regular,704 Menlo,705 monospace706 );707 --trackio-font-weight-normal: 400;708 --trackio-font-weight-medium: 600;709 --trackio-font-weight-bold: 700;710 711 /* Apply font-family to root element */712 font-family: var(--trackio-font-family);713 714 /* Base color system for Classic theme */715 --trackio-base: #323232;716 --trackio-primary: var(--trackio-base);717 --trackio-dim: color-mix(in srgb, var(--trackio-base) 28%, transparent);718 --trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);719 --trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);720 721 /* Chart rendering */722 --trackio-chart-grid-type: "lines"; /* 'lines' | 'dots' */723 --trackio-chart-axis-stroke: var(--trackio-dim);724 --trackio-chart-axis-text: var(--trackio-text);725 --trackio-chart-grid-stroke: var(--trackio-subtle);726 --trackio-chart-grid-opacity: 1;727 }728 729 /* Dark mode overrides for Classic theme */730 :global([data-theme="dark"]) .trackio.theme--classic {731 --trackio-base: #ffffff;732 --trackio-primary: var(--trackio-base);733 --trackio-dim: color-mix(in srgb, var(--trackio-base) 25%, transparent);734 --trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);735 --trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);736 737 /* Cell background for dark mode */738 --trackio-cell-background: rgba(255, 255, 255, 0.03);739 }740 741 .trackio.theme--classic {742 /* Cell styling */743 --trackio-cell-background: rgba(0, 0, 0, 0.02);744 --trackio-cell-border: var(--border-color, rgba(0, 0, 0, 0.1));745 --trackio-cell-corner-inset: 0px;746 --trackio-cell-gap: 12px;747 748 /* Typography */749 --trackio-text-primary: var(--text-color, rgba(0, 0, 0, 0.9));750 --trackio-text-secondary: var(--muted-color, rgba(0, 0, 0, 0.6));751 --trackio-text-accent: var(--primary-color);752 753 /* Tooltip */754 --trackio-tooltip-background: var(--surface-bg, white);755 --trackio-tooltip-border: var(--border-color, rgba(0, 0, 0, 0.1));756 --trackio-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);757 758 /* Legend */759 --trackio-legend-text: var(--text-color, rgba(0, 0, 0, 0.9));760 --trackio-legend-swatch-border: var(--border-color, rgba(0, 0, 0, 0.1));761 }762 763 /* Dark mode adjustments */764 :global([data-theme="dark"]) .trackio {765 --trackio-chart-axis-stroke: rgba(255, 255, 255, 0.18);766 --trackio-chart-axis-text: rgba(255, 255, 255, 0.6);767 --trackio-chart-grid-stroke: rgba(255, 255, 255, 0.08);768 }769 770 /* =========================771 THEME: CLASSIC (Default)772 ========================= */773 774 .trackio.theme--classic {775 /* Keep default values - no overrides needed */776 }777 778 /* =========================779 THEME: OBLIVION780 ========================= */781 782 .trackio.theme--oblivion {783 /* Core oblivion color system - Light mode: darker colors for visibility */784 --trackio-oblivion-base: #2a2a2a;785 --trackio-oblivion-primary: var(--trackio-oblivion-base);786 --trackio-oblivion-dim: color-mix(787 in srgb,788 var(--trackio-oblivion-base) 30%,789 transparent790 );791 --trackio-oblivion-subtle: color-mix(792 in srgb,793 var(--trackio-oblivion-base) 8%,794 transparent795 );796 --trackio-oblivion-ghost: color-mix(797 in srgb,798 var(--trackio-oblivion-base) 4%,799 transparent800 );801 802 /* Chart rendering overrides */803 --trackio-chart-grid-type: "dots";804 --trackio-chart-axis-stroke: var(--trackio-oblivion-dim);805 --trackio-chart-axis-text: var(--trackio-oblivion-primary);806 --trackio-chart-grid-stroke: var(--trackio-oblivion-dim);807 --trackio-chart-grid-opacity: 0.6;808 }809 810 /* Dark mode overrides for Oblivion theme */811 :global([data-theme="dark"]) .trackio.theme--oblivion {812 --trackio-oblivion-base: #ffffff;813 --trackio-oblivion-primary: var(--trackio-oblivion-base);814 --trackio-oblivion-dim: color-mix(815 in srgb,816 var(--trackio-oblivion-base) 25%,817 transparent818 );819 --trackio-oblivion-subtle: color-mix(820 in srgb,821 var(--trackio-oblivion-base) 8%,822 transparent823 );824 --trackio-oblivion-ghost: color-mix(825 in srgb,826 var(--trackio-oblivion-base) 4%,827 transparent828 );829 }830 831 .trackio.theme--oblivion {832 /* Cell styling overrides */833 --trackio-cell-background: var(--trackio-oblivion-subtle);834 --trackio-cell-border: var(--trackio-oblivion-dim);835 --trackio-cell-corner-inset: 6px;836 --trackio-cell-gap: 0px;837 838 /* HUD-specific variables */839 --trackio-oblivion-hud-gap: 10px;840 --trackio-oblivion-hud-corner-size: 8px;841 --trackio-oblivion-hud-bg-gradient: radial-gradient(842 1200px 200px at 20% -10%,843 var(--trackio-oblivion-ghost),844 transparent 80%845 ),846 radial-gradient(847 900px 200px at 80% 110%,848 var(--trackio-oblivion-ghost),849 transparent 80%850 );851 852 /* Typography overrides */853 --trackio-text-primary: var(--trackio-oblivion-primary);854 --trackio-text-secondary: var(--trackio-oblivion-dim);855 --trackio-text-accent: var(--trackio-oblivion-primary);856 857 /* Tooltip overrides */858 --trackio-tooltip-background: var(--trackio-oblivion-subtle);859 --trackio-tooltip-border: var(--trackio-oblivion-dim);860 --trackio-tooltip-shadow: 0 8px 32px861 color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent),862 0 2px 8px color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent);863 864 /* Legend overrides */865 --trackio-legend-text: var(--trackio-oblivion-primary);866 --trackio-legend-swatch-border: var(--trackio-oblivion-dim);867 868 /* Font styling overrides */869 --trackio-font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,870 SFMono-Regular, Menlo, monospace;871 font-family: var(--trackio-font-family) !important;872 color: var(--trackio-text-primary);873 }874 875 /* Force Roboto Mono application in Oblivion theme */876 .trackio.theme--oblivion,877 .trackio.theme--oblivion * {878 font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,879 SFMono-Regular, Menlo, monospace !important;880 }881 882 /* Specific overrides for different elements in Oblivion */883 .trackio.theme--oblivion .cell-title,884 .trackio.theme--oblivion .legend-bottom,885 .trackio.theme--oblivion .legend-title,886 .trackio.theme--oblivion .item {887 font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,888 SFMono-Regular, Menlo, monospace !important;889 }890 891 /* Dark mode adjustments for Oblivion */892 :global([data-theme="dark"]) .trackio.theme--oblivion {893 --trackio-oblivion-base: #ffffff;894 --trackio-oblivion-hud-bg-gradient: radial-gradient(895 1400px 260px at 20% -10%,896 color-mix(in srgb, var(--trackio-oblivion-base) 6.5%, transparent),897 transparent 80%898 ),899 radial-gradient(900 1100px 240px at 80% 110%,901 color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent),902 transparent 80%903 ),904 linear-gradient(905 180deg,906 color-mix(in srgb, var(--trackio-oblivion-base) 3.5%, transparent),907 transparent 45%908 );909 910 --trackio-tooltip-shadow: 0 8px 32px911 color-mix(in srgb, var(--trackio-oblivion-base) 5%, transparent),912 0 2px 8px color-mix(in srgb, black 10%, transparent);913 914 background: #0f1115;915 }916 917 /* =========================918 LAYOUT & COMPONENTS919 ========================= */920 921 .trackio__grid {922 display: grid;923 grid-template-columns: repeat(2, minmax(0, 1fr));924 gap: var(--trackio-cell-gap);925 }926 927 @media (max-width: 980px) {928 .trackio__grid {929 grid-template-columns: 1fr;930 }931 }932 933 .trackio__header {934 display: flex;935 align-items: flex-start;936 justify-content: center;937 gap: 12px;938 margin: 0 0 10px 0;939 flex-wrap: wrap;940 width: 100%;941 }942 943 /* Legacy axis/grid selectors - for compatibility with Cell.svelte */944 .trackio .axes path,945 .trackio .axes line {946 stroke: var(--trackio-chart-axis-stroke);947 }948 949 .trackio .axes text {950 fill: var(--trackio-chart-axis-text);951 font-family: var(--trackio-font-family);952 }953 954 /* Force font-family for SVG text in Oblivion */955 .trackio.theme--oblivion .axes text {956 font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,957 SFMono-Regular, Menlo, monospace !important;958 }959 960 .trackio .grid line {961 stroke: var(--trackio-chart-grid-stroke);962 opacity: var(--trackio-chart-grid-opacity);963 }964 965 /* Grid type switching */966 .trackio .grid-dots {967 display: none;968 }969 .trackio.theme--oblivion .grid {970 display: none;971 }972 .trackio.theme--oblivion .grid-dots {973 display: block;974 }975 .trackio.theme--oblivion .cell-bg,976 .trackio.theme--oblivion .cell-corners {977 display: block;978 }979 980 /* =========================981 FOOTER982 ========================= */983 984 .trackio__footer {985 display: flex;986 justify-content: center;987 align-items: center;988 margin-top: 12px;989 padding-top: 6px;990 opacity: 1;991 }992 993 .trackio__footer small {994 font-size: 10px;995 color: var(--trackio-text-secondary);996 font-family: var(--trackio-font-family);997 opacity: 0.7;998 }999 1000 .trackio__footer a {1001 color: var(--trackio-text-secondary);1002 text-decoration: none;1003 border-top: 1px solid var(--trackio-chart-grid-stroke);1004 font-weight: var(--trackio-font-weight-normal);1005 transition: opacity 0.15s ease;1006 }1007 1008 .trackio__footer a:hover {1009 text-decoration: none;1010 }1011 1012 .trackio__footer .separator {1013 margin: 0 6px;1014 }1015 1016 /* Oblivion theme footer adjustments */1017 .trackio.theme--oblivion .trackio__footer {1018 border-top-color: var(--trackio-oblivion-dim);1019 }1020 1021 .trackio.theme--oblivion .trackio__footer small {1022 font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,1023 SFMono-Regular, Menlo, monospace !important;1024 }1025</style>1026 