Molbap's picture
Molbap HF Staff
push a bunch of updates
e903a32
raw
history blame
31 kB
<script>
import * as d3 from "d3";
import { formatAbbrev, smoothMetricData } from "./core/chart-utils.js";
import {
generateRunNames,
genCurves,
Random,
Performance,
generateMassiveTestDataset,
} from "./core/data-generator.js";
import Legend from "./components/Legend.svelte";
import Cell from "./components/Cell.svelte";
import FullscreenModal from "./components/FullscreenModal.svelte";
import { onMount, onDestroy } from "svelte";
import { jitterTrigger } from "./core/store.js";
export let variant = "classic"; // 'classic' | 'oblivion'
export let normalizeLoss = true;
export let logScaleX = false;
export let smoothing = false;
let hostEl;
let gridEl;
let legendItems = [];
const cellsDef = [
{ metric: "epoch", title: "Epoch" },
{ metric: "train_accuracy", title: "Train accuracy" },
{ metric: "train_loss", title: "Train loss" },
{ metric: "val_accuracy", title: "Val accuracy" },
{ metric: "val_loss", title: "Val loss", wide: true },
];
let preparedData = {};
let colorsByRun = {};
// Variables for data management (will be initialized in onMount)
let dataByMetric = new Map();
let metricsToDraw = [];
let currentRunList = [];
let cycleIdx = 2;
// Dynamic color palette using color-palettes.js helper
let dynamicPalette = [
"#0ea5e9",
"#8b5cf6",
"#f59e0b",
"#ef4444",
"#10b981",
"#f97316",
"#3b82f6",
"#8b5ad6",
]; // fallback
const updateDynamicPalette = () => {
if (
typeof window !== "undefined" &&
window.ColorPalettes &&
currentRunList.length > 0
) {
try {
dynamicPalette = window.ColorPalettes.getColors(
"categorical",
currentRunList.length,
);
} catch (e) {
console.warn("Failed to generate dynamic palette:", e);
// Keep fallback palette
}
}
};
const colorForRun = (name) => {
const idx = currentRunList.indexOf(name);
return idx >= 0 ? dynamicPalette[idx % dynamicPalette.length] : "#999";
};
// Jitter function - generates completely new data with new runs
function jitterData() {
console.log(
"jitterData called - generating new data with random number of runs",
); // Debug log
// Generate new random data with weighted probability for fewer runs
// Higher probability for 2-3 runs, lower for 4-5-6 runs
const rand = Math.random();
let wantRuns;
if (rand < 0.4)
wantRuns = 2; // 40% chance
else if (rand < 0.7)
wantRuns = 3; // 30% chance
else if (rand < 0.85)
wantRuns = 4; // 15% chance
else if (rand < 0.95)
wantRuns = 5; // 10% chance
else wantRuns = 6; // 5% chance
// Use realistic ML training step counts
const stepsCount = Random.trainingSteps();
const runsSim = generateRunNames(wantRuns, stepsCount);
const steps = Array.from({ length: stepsCount }, (_, i) => i + 1);
const nextByMetric = new Map();
const TARGET_METRICS = [
"epoch",
"train_accuracy",
"train_loss",
"val_accuracy",
"val_loss",
];
// Initialize data structure
TARGET_METRICS.forEach((tgt) => {
const map = {};
runsSim.forEach((r) => {
map[r] = [];
});
nextByMetric.set(tgt, map);
});
// Generate curves for each run
runsSim.forEach((run) => {
const curves = genCurves(stepsCount);
steps.forEach((s, i) => {
nextByMetric.get("epoch")[run].push({ step: s, value: s });
nextByMetric
.get("train_accuracy")
[run].push({ step: s, value: curves.accTrain[i] });
nextByMetric
.get("val_accuracy")
[run].push({ step: s, value: curves.accVal[i] });
nextByMetric
.get("train_loss")
[run].push({ step: s, value: curves.lossTrain[i] });
nextByMetric
.get("val_loss")
[run].push({ step: s, value: curves.lossVal[i] });
});
});
// Update all reactive data
nextByMetric.forEach((v, k) => dataByMetric.set(k, v));
metricsToDraw = TARGET_METRICS;
currentRunList = runsSim.slice();
updateDynamicPalette(); // Generate new colors based on run count
legendItems = currentRunList.map((name) => ({
name,
color: colorForRun(name),
}));
updatePreparedData();
colorsByRun = Object.fromEntries(
currentRunList.map((name) => [name, colorForRun(name)]),
);
console.log(
`jitterData completed - generated ${wantRuns} runs with ${stepsCount} steps`,
); // Debug log
}
// Public API: allow external theme switch
function setTheme(name) {
variant = name === "oblivion" ? "oblivion" : "classic";
updateThemeClass();
// Debug log for font application
if (typeof window !== "undefined") {
console.log(`Theme switched to: ${variant}`);
if (hostEl) {
const computedStyle = getComputedStyle(hostEl);
const appliedFont = computedStyle.fontFamily;
console.log(`Applied font-family: ${appliedFont}`);
}
}
}
// Public API: allow external log scale X toggle
function setLogScaleX(enabled) {
logScaleX = enabled;
console.log(`Log scale X set to: ${logScaleX}`);
}
// Public API: allow external smoothing toggle
function setSmoothing(enabled) {
smoothing = enabled;
console.log(`Smoothing set to: ${smoothing}`);
// Re-prepare data with smoothing applied
updatePreparedData();
}
// Public API: generate massive test dataset
function generateMassiveDataset(steps = null, runs = 3) {
console.log(
"🧪 Generating massive test dataset for sampling validation...",
);
const result = generateMassiveTestDataset(steps, runs);
// Update reactive data with massive dataset
result.dataByMetric.forEach((v, k) => dataByMetric.set(k, v));
metricsToDraw = [
"epoch",
"train_accuracy",
"train_loss",
"val_accuracy",
"val_loss",
];
currentRunList = result.runNames.slice();
updateDynamicPalette();
legendItems = currentRunList.map((name) => ({
name,
color: colorForRun(name),
}));
updatePreparedData();
colorsByRun = Object.fromEntries(
currentRunList.map((name) => [name, colorForRun(name)]),
);
console.log(
`✅ Massive dataset loaded: ${result.stepCount} steps × ${result.runNames.length} runs`,
);
console.log(`📊 Total data points: ${result.totalPoints.toLocaleString()}`);
console.log(`🎯 Description: ${result.description}`);
return result;
}
// Public API: add live data point for simulation
function addLiveDataPoint(runName, dataPoint) {
console.log(`Adding live data point for run "${runName}":`, dataPoint);
// Add run to currentRunList if it doesn't exist
if (!currentRunList.includes(runName)) {
currentRunList = [...currentRunList, runName];
updateDynamicPalette();
colorsByRun = Object.fromEntries(
currentRunList.map((name) => [name, colorForRun(name)]),
);
legendItems = currentRunList.map((name) => ({
name,
color: colorForRun(name),
}));
}
// Initialize data structures for the run if needed
const TARGET_METRICS = [
"epoch",
"train_accuracy",
"train_loss",
"val_accuracy",
"val_loss",
];
TARGET_METRICS.forEach((metric) => {
if (!dataByMetric.has(metric)) {
dataByMetric.set(metric, {});
}
const metricData = dataByMetric.get(metric);
if (!metricData[runName]) {
metricData[runName] = [];
}
});
// Add the new data points to each metric
const step = dataPoint.step;
// Add epoch data
const epochData = dataByMetric.get("epoch");
epochData[runName].push({ step, value: step });
// Add accuracy data (train and val get the same value for simplicity)
if (dataPoint.accuracy !== undefined) {
const trainAccData = dataByMetric.get("train_accuracy");
const valAccData = dataByMetric.get("val_accuracy");
// Add some noise between train and val accuracy
const trainAcc = dataPoint.accuracy;
const valAcc = Math.max(
0,
Math.min(1, dataPoint.accuracy - 0.01 - Math.random() * 0.03),
);
trainAccData[runName].push({ step, value: trainAcc });
valAccData[runName].push({ step, value: valAcc });
}
// Add loss data (train and val get the same value for simplicity)
if (dataPoint.loss !== undefined) {
const trainLossData = dataByMetric.get("train_loss");
const valLossData = dataByMetric.get("val_loss");
// Add some noise between train and val loss
const trainLoss = dataPoint.loss;
const valLoss = dataPoint.loss + 0.05 + Math.random() * 0.1;
trainLossData[runName].push({ step, value: trainLoss });
valLossData[runName].push({ step, value: valLoss });
}
// Update all metrics to draw
metricsToDraw = TARGET_METRICS;
// Update prepared data with new values
updatePreparedData();
console.log(
`Live data point added successfully. Total runs: ${currentRunList.length}`,
);
}
// Update prepared data with optional smoothing
let preparedRawData = {}; // Store original data for background display
function updatePreparedData() {
const TARGET_METRICS = [
"epoch",
"train_accuracy",
"train_loss",
"val_accuracy",
"val_loss",
];
let dataToUse = {};
let rawDataToStore = {};
TARGET_METRICS.forEach((metric) => {
const rawData = dataByMetric.get(metric);
if (rawData) {
// Store original data
rawDataToStore[metric] = rawData;
// Apply smoothing if enabled (except for epoch which should stay exact)
dataToUse[metric] =
smoothing && metric !== "epoch"
? smoothMetricData(rawData, 5) // Window size of 5
: rawData;
}
});
preparedData = dataToUse;
preparedRawData = rawDataToStore;
console.log(`Prepared data updated, smoothing: ${smoothing}`);
}
function updateThemeClass() {
if (!hostEl) return;
hostEl.classList.toggle("theme--classic", variant === "classic");
hostEl.classList.toggle("theme--oblivion", variant === "oblivion");
hostEl.setAttribute("data-variant", variant);
}
$: updateThemeClass();
// Chart logic now handled by Cell.svelte
// Fullscreen navigation state
let currentFullscreenIndex = 0;
let isModalOpen = false;
function handleNavigate(newIndex) {
currentFullscreenIndex = newIndex;
}
function openModal(index) {
currentFullscreenIndex = index;
isModalOpen = true;
}
function closeModal() {
isModalOpen = false;
}
// Prepare all charts data for navigation
$: allChartsData = cellsDef.map((c) => ({
metricKey: c.metric,
titleText: c.title,
metricData: (preparedData && preparedData[c.metric]) || {},
rawMetricData: (preparedRawData && preparedRawData[c.metric]) || {},
}));
// Color function for the modal
$: modalColorForRun = (name) => colorsByRun[name] || "#999";
let cleanup = null;
onMount(() => {
if (!hostEl || !gridEl) return;
hostEl.__setTheme = setTheme;
// Jitter & Simulate functions
function rebuildLegend() {
updateDynamicPalette(); // Update colors when adding new data
legendItems = currentRunList.map((name) => ({
name,
color: colorForRun(name),
}));
}
function simulateData() {
// Generate new random data with weighted probability for fewer runs
// Higher probability for 2-3 runs, lower for 4-5-6 runs
const rand = Math.random();
let wantRuns;
if (rand < 0.4)
wantRuns = 2; // 40% chance
else if (rand < 0.7)
wantRuns = 3; // 30% chance
else if (rand < 0.85)
wantRuns = 4; // 15% chance
else if (rand < 0.95)
wantRuns = 5; // 10% chance
else wantRuns = 6; // 5% chance
// Use realistic ML training step counts with cycling scenarios
let stepsCount;
if (cycleIdx === 0) {
stepsCount = Random.trainingStepsForScenario("prototyping");
} else if (cycleIdx === 1) {
stepsCount = Random.trainingStepsForScenario("development");
} else if (cycleIdx === 2) {
stepsCount = Random.trainingStepsForScenario("production");
} else if (cycleIdx === 3) {
stepsCount = Random.trainingStepsForScenario("research");
} else if (cycleIdx === 4) {
stepsCount = Random.trainingStepsForScenario("llm");
} else if (cycleIdx === 5) {
stepsCount = Random.trainingStepsForScenario("massive");
} else {
stepsCount = Random.trainingSteps(); // Full range for variety
}
cycleIdx = (cycleIdx + 1) % 7; // Cycle through 7 scenarios now
const runsSim = generateRunNames(wantRuns, stepsCount);
const steps = Array.from({ length: stepsCount }, (_, i) => i + 1);
const nextByMetric = new Map();
const TARGET_METRICS = [
"epoch",
"train_accuracy",
"train_loss",
"val_accuracy",
"val_loss",
];
const mList =
metricsToDraw && metricsToDraw.length ? metricsToDraw : TARGET_METRICS;
mList.forEach((tgt) => {
const map = {};
runsSim.forEach((r) => {
map[r] = [];
});
nextByMetric.set(tgt, map);
});
runsSim.forEach((run) => {
const curves = genCurves(stepsCount);
steps.forEach((s, i) => {
if (mList.includes("epoch"))
nextByMetric.get("epoch")[run].push({ step: s, value: s });
if (mList.includes("train_accuracy"))
nextByMetric
.get("train_accuracy")
[run].push({ step: s, value: curves.accTrain[i] });
if (mList.includes("val_accuracy"))
nextByMetric
.get("val_accuracy")
[run].push({ step: s, value: curves.accVal[i] });
if (mList.includes("train_loss"))
nextByMetric
.get("train_loss")
[run].push({ step: s, value: curves.lossTrain[i] });
if (mList.includes("val_loss"))
nextByMetric
.get("val_loss")
[run].push({ step: s, value: curves.lossVal[i] });
});
});
nextByMetric.forEach((v, k) => dataByMetric.set(k, v));
currentRunList = runsSim.slice();
rebuildLegend();
updatePreparedData();
updateDynamicPalette(); // Update colors when rebuilding
colorsByRun = Object.fromEntries(
currentRunList.map((name) => [name, colorForRun(name)]),
);
}
// No need for event listeners anymore - we'll use reactive statement
// Start with level 3 long synthetic data for consistency
simulateData();
// Svelte Cells will react to preparedData/colorsByRun updates
cleanup = () => {
// No cleanup needed for reactive statements
};
});
onDestroy(() => {
if (cleanup) cleanup();
});
// Expose instance for debugging and external theme control
onMount(() => {
window.trackioInstance = {
jitterData,
addLiveDataPoint,
generateMassiveDataset,
};
if (hostEl) {
hostEl.__trackioInstance = {
setTheme,
setLogScaleX,
setSmoothing,
jitterData,
addLiveDataPoint,
generateMassiveDataset,
};
}
// Initialize dynamic palette
updateDynamicPalette();
// Listen for palette updates from color-palettes.js
const handlePaletteUpdate = () => {
updateDynamicPalette();
// Rebuild legend and colors if needed
if (currentRunList.length > 0) {
legendItems = currentRunList.map((name) => ({
name,
color: colorForRun(name),
}));
colorsByRun = Object.fromEntries(
currentRunList.map((name) => [name, colorForRun(name)]),
);
}
};
document.addEventListener("palettes:updated", handlePaletteUpdate);
// Cleanup listener on destroy
return () => {
document.removeEventListener("palettes:updated", handlePaletteUpdate);
};
});
// React to jitter trigger from store
$: {
console.log(
"Reactive statement triggered, jitterTrigger value:",
$jitterTrigger,
);
if ($jitterTrigger > 0) {
console.log(
"Jitter trigger activated:",
$jitterTrigger,
"calling jitterData()",
);
jitterData();
}
}
// Legend ghost helpers (hover effects)
function ghostRun(run) {
try {
hostEl.classList.add("hovering");
// Ghost the chart lines and points
hostEl.querySelectorAll(".cell").forEach((cell) => {
cell
.querySelectorAll("svg .lines path.run-line")
.forEach((p) =>
p.classList.toggle("ghost", p.getAttribute("data-run") !== run),
);
cell
.querySelectorAll("svg .lines path.raw-line")
.forEach((p) =>
p.classList.toggle("ghost", p.getAttribute("data-run") !== run),
);
cell
.querySelectorAll("svg .points circle.pt")
.forEach((c) =>
c.classList.toggle("ghost", c.getAttribute("data-run") !== run),
);
});
// Ghost the legend items
hostEl.querySelectorAll(".legend-bottom .item").forEach((item) => {
const itemRun = item.getAttribute("data-run");
item.classList.toggle("ghost", itemRun !== run);
});
} catch (_) {}
}
function clearGhost() {
try {
hostEl.classList.remove("hovering");
// Clear ghost from chart lines and points
hostEl.querySelectorAll(".cell").forEach((cell) => {
cell
.querySelectorAll("svg .lines path.run-line")
.forEach((p) => p.classList.remove("ghost"));
cell
.querySelectorAll("svg .lines path.raw-line")
.forEach((p) => p.classList.remove("ghost"));
cell
.querySelectorAll("svg .points circle.pt")
.forEach((c) => c.classList.remove("ghost"));
});
// Clear ghost from legend items
hostEl.querySelectorAll(".legend-bottom .item").forEach((item) => {
item.classList.remove("ghost");
});
} catch (_) {}
}
</script>
<div class="trackio theme--classic" bind:this={hostEl} data-variant={variant}>
<div class="trackio__header">
<Legend
items={legendItems}
on:legend-hover={(e) => {
const run = e?.detail?.name;
if (!run) return;
ghostRun(run);
}}
on:legend-leave={() => {
clearGhost();
}}
/>
</div>
<div class="trackio__grid" bind:this={gridEl}>
{#each cellsDef as c, i}
<Cell
metricKey={c.metric}
titleText={c.title}
wide={c.wide}
{variant}
{normalizeLoss}
{logScaleX}
{smoothing}
metricData={(preparedData && preparedData[c.metric]) || {}}
rawMetricData={(preparedRawData && preparedRawData[c.metric]) || {}}
colorForRun={(name) => colorsByRun[name] || "#999"}
{hostEl}
currentIndex={i}
onOpenModal={openModal}
/>
{/each}
</div>
<div class="trackio__footer">
<small>
Built with <a
href="https://github.com/huggingface/trackio"
target="_blank"
rel="noopener noreferrer">TrackIO</a
>
<span class="separator"></span>
<a
href="https://huggingface.co/docs/hub/spaces-sdks-docker"
target="_blank"
rel="noopener noreferrer">Use via API</a
>
</small>
</div>
</div>
<!-- Centralized Fullscreen Modal -->
<FullscreenModal
visible={isModalOpen}
title={allChartsData[currentFullscreenIndex]?.titleText || ""}
metricData={allChartsData[currentFullscreenIndex]?.metricData || {}}
rawMetricData={allChartsData[currentFullscreenIndex]?.rawMetricData || {}}
colorForRun={modalColorForRun}
{variant}
{logScaleX}
{smoothing}
{normalizeLoss}
metricKey={allChartsData[currentFullscreenIndex]?.metricKey || ""}
titleText={allChartsData[currentFullscreenIndex]?.titleText || ""}
currentIndex={currentFullscreenIndex}
totalCharts={cellsDef.length}
onNavigate={handleNavigate}
on:close={closeModal}
/>
<style>
/* =========================
TRACKIO THEME SYSTEM
========================= */
/* Font imports for themes - ensure Roboto Mono is loaded for Oblivion theme */
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap");
/* Fallback font-face declaration */
@font-face {
font-family: "Roboto Mono Fallback";
src: url("https://fonts.gstatic.com/s/robotomono/v23/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW4AJi8SJQt.woff2")
format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* Base variables - all themes inherit these */
.trackio {
position: relative;
--z-tooltip: 50;
--z-overlay: 99999999;
/* Typography */
--trackio-font-family: var(
--font-mono,
ui-monospace,
SFMono-Regular,
Menlo,
monospace
);
--trackio-font-weight-normal: 400;
--trackio-font-weight-medium: 600;
--trackio-font-weight-bold: 700;
/* Apply font-family to root element */
font-family: var(--trackio-font-family);
/* Base color system for Classic theme */
--trackio-base: #323232;
--trackio-primary: var(--trackio-base);
--trackio-dim: color-mix(in srgb, var(--trackio-base) 28%, transparent);
--trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
--trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
/* Chart rendering */
--trackio-chart-grid-type: "lines"; /* 'lines' | 'dots' */
--trackio-chart-axis-stroke: var(--trackio-dim);
--trackio-chart-axis-text: var(--trackio-text);
--trackio-chart-grid-stroke: var(--trackio-subtle);
--trackio-chart-grid-opacity: 1;
}
/* Dark mode overrides for Classic theme */
:global([data-theme="dark"]) .trackio.theme--classic {
--trackio-base: #ffffff;
--trackio-primary: var(--trackio-base);
--trackio-dim: color-mix(in srgb, var(--trackio-base) 25%, transparent);
--trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
--trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
/* Cell background for dark mode */
--trackio-cell-background: rgba(255, 255, 255, 0.03);
}
.trackio.theme--classic {
/* Cell styling */
--trackio-cell-background: rgba(0, 0, 0, 0.02);
--trackio-cell-border: var(--border-color, rgba(0, 0, 0, 0.1));
--trackio-cell-corner-inset: 0px;
--trackio-cell-gap: 12px;
/* Typography */
--trackio-text-primary: var(--text-color, rgba(0, 0, 0, 0.9));
--trackio-text-secondary: var(--muted-color, rgba(0, 0, 0, 0.6));
--trackio-text-accent: var(--primary-color);
/* Tooltip */
--trackio-tooltip-background: var(--surface-bg, white);
--trackio-tooltip-border: var(--border-color, rgba(0, 0, 0, 0.1));
--trackio-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
/* Legend */
--trackio-legend-text: var(--text-color, rgba(0, 0, 0, 0.9));
--trackio-legend-swatch-border: var(--border-color, rgba(0, 0, 0, 0.1));
}
/* Dark mode adjustments */
:global([data-theme="dark"]) .trackio {
--trackio-chart-axis-stroke: rgba(255, 255, 255, 0.18);
--trackio-chart-axis-text: rgba(255, 255, 255, 0.6);
--trackio-chart-grid-stroke: rgba(255, 255, 255, 0.08);
}
/* =========================
THEME: CLASSIC (Default)
========================= */
.trackio.theme--classic {
/* Keep default values - no overrides needed */
}
/* =========================
THEME: OBLIVION
========================= */
.trackio.theme--oblivion {
/* Core oblivion color system - Light mode: darker colors for visibility */
--trackio-oblivion-base: #2a2a2a;
--trackio-oblivion-primary: var(--trackio-oblivion-base);
--trackio-oblivion-dim: color-mix(
in srgb,
var(--trackio-oblivion-base) 30%,
transparent
);
--trackio-oblivion-subtle: color-mix(
in srgb,
var(--trackio-oblivion-base) 8%,
transparent
);
--trackio-oblivion-ghost: color-mix(
in srgb,
var(--trackio-oblivion-base) 4%,
transparent
);
/* Chart rendering overrides */
--trackio-chart-grid-type: "dots";
--trackio-chart-axis-stroke: var(--trackio-oblivion-dim);
--trackio-chart-axis-text: var(--trackio-oblivion-primary);
--trackio-chart-grid-stroke: var(--trackio-oblivion-dim);
--trackio-chart-grid-opacity: 0.6;
}
/* Dark mode overrides for Oblivion theme */
:global([data-theme="dark"]) .trackio.theme--oblivion {
--trackio-oblivion-base: #ffffff;
--trackio-oblivion-primary: var(--trackio-oblivion-base);
--trackio-oblivion-dim: color-mix(
in srgb,
var(--trackio-oblivion-base) 25%,
transparent
);
--trackio-oblivion-subtle: color-mix(
in srgb,
var(--trackio-oblivion-base) 8%,
transparent
);
--trackio-oblivion-ghost: color-mix(
in srgb,
var(--trackio-oblivion-base) 4%,
transparent
);
}
.trackio.theme--oblivion {
/* Cell styling overrides */
--trackio-cell-background: var(--trackio-oblivion-subtle);
--trackio-cell-border: var(--trackio-oblivion-dim);
--trackio-cell-corner-inset: 6px;
--trackio-cell-gap: 0px;
/* HUD-specific variables */
--trackio-oblivion-hud-gap: 10px;
--trackio-oblivion-hud-corner-size: 8px;
--trackio-oblivion-hud-bg-gradient: radial-gradient(
1200px 200px at 20% -10%,
var(--trackio-oblivion-ghost),
transparent 80%
),
radial-gradient(
900px 200px at 80% 110%,
var(--trackio-oblivion-ghost),
transparent 80%
);
/* Typography overrides */
--trackio-text-primary: var(--trackio-oblivion-primary);
--trackio-text-secondary: var(--trackio-oblivion-dim);
--trackio-text-accent: var(--trackio-oblivion-primary);
/* Tooltip overrides */
--trackio-tooltip-background: var(--trackio-oblivion-subtle);
--trackio-tooltip-border: var(--trackio-oblivion-dim);
--trackio-tooltip-shadow: 0 8px 32px
color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent),
0 2px 8px color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent);
/* Legend overrides */
--trackio-legend-text: var(--trackio-oblivion-primary);
--trackio-legend-swatch-border: var(--trackio-oblivion-dim);
/* Font styling overrides */
--trackio-font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
SFMono-Regular, Menlo, monospace;
font-family: var(--trackio-font-family) !important;
color: var(--trackio-text-primary);
}
/* Force Roboto Mono application in Oblivion theme */
.trackio.theme--oblivion,
.trackio.theme--oblivion * {
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
SFMono-Regular, Menlo, monospace !important;
}
/* Specific overrides for different elements in Oblivion */
.trackio.theme--oblivion .cell-title,
.trackio.theme--oblivion .legend-bottom,
.trackio.theme--oblivion .legend-title,
.trackio.theme--oblivion .item {
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
SFMono-Regular, Menlo, monospace !important;
}
/* Dark mode adjustments for Oblivion */
:global([data-theme="dark"]) .trackio.theme--oblivion {
--trackio-oblivion-base: #ffffff;
--trackio-oblivion-hud-bg-gradient: radial-gradient(
1400px 260px at 20% -10%,
color-mix(in srgb, var(--trackio-oblivion-base) 6.5%, transparent),
transparent 80%
),
radial-gradient(
1100px 240px at 80% 110%,
color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent),
transparent 80%
),
linear-gradient(
180deg,
color-mix(in srgb, var(--trackio-oblivion-base) 3.5%, transparent),
transparent 45%
);
--trackio-tooltip-shadow: 0 8px 32px
color-mix(in srgb, var(--trackio-oblivion-base) 5%, transparent),
0 2px 8px color-mix(in srgb, black 10%, transparent);
background: #0f1115;
}
/* =========================
LAYOUT & COMPONENTS
========================= */
.trackio__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--trackio-cell-gap);
}
@media (max-width: 980px) {
.trackio__grid {
grid-template-columns: 1fr;
}
}
.trackio__header {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 12px;
margin: 0 0 10px 0;
flex-wrap: wrap;
width: 100%;
}
/* Legacy axis/grid selectors - for compatibility with Cell.svelte */
.trackio .axes path,
.trackio .axes line {
stroke: var(--trackio-chart-axis-stroke);
}
.trackio .axes text {
fill: var(--trackio-chart-axis-text);
font-family: var(--trackio-font-family);
}
/* Force font-family for SVG text in Oblivion */
.trackio.theme--oblivion .axes text {
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
SFMono-Regular, Menlo, monospace !important;
}
.trackio .grid line {
stroke: var(--trackio-chart-grid-stroke);
opacity: var(--trackio-chart-grid-opacity);
}
/* Grid type switching */
.trackio .grid-dots {
display: none;
}
.trackio.theme--oblivion .grid {
display: none;
}
.trackio.theme--oblivion .grid-dots {
display: block;
}
.trackio.theme--oblivion .cell-bg,
.trackio.theme--oblivion .cell-corners {
display: block;
}
/* =========================
FOOTER
========================= */
.trackio__footer {
display: flex;
justify-content: center;
align-items: center;
margin-top: 12px;
padding-top: 6px;
opacity: 1;
}
.trackio__footer small {
font-size: 10px;
color: var(--trackio-text-secondary);
font-family: var(--trackio-font-family);
opacity: 0.7;
}
.trackio__footer a {
color: var(--trackio-text-secondary);
text-decoration: none;
border-top: 1px solid var(--trackio-chart-grid-stroke);
font-weight: var(--trackio-font-weight-normal);
transition: opacity 0.15s ease;
}
.trackio__footer a:hover {
text-decoration: none;
}
.trackio__footer .separator {
margin: 0 6px;
}
/* Oblivion theme footer adjustments */
.trackio.theme--oblivion .trackio__footer {
border-top-color: var(--trackio-oblivion-dim);
}
.trackio.theme--oblivion .trackio__footer small {
font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace,
SFMono-Regular, Menlo, monospace !important;
}
</style>