| <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"; |
| 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 = {}; |
| |
| |
| let dataByMetric = new Map(); |
| let metricsToDraw = []; |
| let currentRunList = []; |
| let cycleIdx = 2; |
| |
| |
| let dynamicPalette = [ |
| "#0ea5e9", |
| "#8b5cf6", |
| "#f59e0b", |
| "#ef4444", |
| "#10b981", |
| "#f97316", |
| "#3b82f6", |
| "#8b5ad6", |
| ]; |
| |
| 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"; |
| }; |
| |
| |
| 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", |
| ]; |
| |
| |
| TARGET_METRICS.forEach((tgt) => { |
| const map = {}; |
| runsSim.forEach((r) => { |
| map[r] = []; |
| }); |
| nextByMetric.set(tgt, map); |
| }); |
| |
| |
| 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] }); |
| }); |
| }); |
| |
| |
| nextByMetric.forEach((v, k) => dataByMetric.set(k, v)); |
| metricsToDraw = TARGET_METRICS; |
| currentRunList = runsSim.slice(); |
| updateDynamicPalette(); |
| 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), |
| })); |
| } |
| |
| |
| 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] = []; |
| } |
| }); |
| |
| |
| const step = dataPoint.step; |
| |
| |
| const epochData = dataByMetric.get("epoch"); |
| epochData[runName].push({ step, value: step }); |
| |
| |
| 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 }); |
| } |
| |
| |
| 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 }); |
| } |
| |
| |
| metricsToDraw = TARGET_METRICS; |
| |
| |
| updatePreparedData(); |
| |
| console.log( |
| `Live data point added successfully. Total runs: ${currentRunList.length}`, |
| ); |
| } |
| |
| // Update prepared data with optional smoothing |
| let preparedRawData = {}; |
| |
| 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(); |
| |
| |
| |
| |
| let currentFullscreenIndex = 0; |
| let isModalOpen = false; |
| |
| function handleNavigate(newIndex) { |
| currentFullscreenIndex = newIndex; |
| } |
| |
| function openModal(index) { |
| currentFullscreenIndex = index; |
| isModalOpen = true; |
| } |
| |
| function closeModal() { |
| isModalOpen = false; |
| } |
| |
| |
| $: allChartsData = cellsDef.map((c) => ({ |
| metricKey: c.metric, |
| titleText: c.title, |
| metricData: (preparedData && preparedData[c.metric]) || {}, |
| rawMetricData: (preparedRawData && preparedRawData[c.metric]) || {}, |
| })); |
| |
| |
| $: 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; |
| |
| 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(); |
| colorsByRun = Object.fromEntries( |
| currentRunList.map((name) => [name, colorForRun(name)]), |
| ); |
| } |
| |
| |
| |
| simulateData(); |
| |
| |
| cleanup = () => { |
| // No cleanup needed for reactive statements |
| }; |
| }); |
| |
| onDestroy(() => { |
| if (cleanup) cleanup(); |
| }); |
| |
| |
| onMount(() => { |
| window.trackioInstance = { |
| jitterData, |
| addLiveDataPoint, |
| generateMassiveDataset, |
| }; |
| if (hostEl) { |
| hostEl.__trackioInstance = { |
| setTheme, |
| setLogScaleX, |
| setSmoothing, |
| jitterData, |
| addLiveDataPoint, |
| generateMassiveDataset, |
| }; |
| } |
| |
| |
| updateDynamicPalette(); |
| |
| |
| 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); |
| |
| |
| return () => { |
| document.removeEventListener("palettes:updated", handlePaletteUpdate); |
| }; |
| }); |
| |
| |
| $: { |
| console.log( |
| "Reactive statement triggered, jitterTrigger value:", |
| $jitterTrigger, |
| ); |
| if ($jitterTrigger > 0) { |
| console.log( |
| "Jitter trigger activated:", |
| $jitterTrigger, |
| "calling jitterData()", |
| ); |
| jitterData(); |
| } |
| } |
| |
| |
| 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), |
| ); |
| }); |
| |
| |
| 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")); |
| }); |
| |
| |
| 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> |
| |
| |
| |
| |
| |
| @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap"); |
| |
| |
| @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; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| :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)); |
| } |
| |
| |
| :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); |
| } |
| |
| |
| |
| |
| |
| .trackio.theme--classic { |
| /* Keep default values - no overrides needed */ |
| } |
| |
| |
| |
| |
| |
| .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; |
| } |
| |
| |
| :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); |
| } |
| |
| |
| .trackio.theme--oblivion, |
| .trackio.theme--oblivion * { |
| font-family: "Roboto Mono", "Roboto Mono Fallback", ui-monospace, |
| SFMono-Regular, Menlo, monospace !important; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| :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; |
| } |
| |
| |
| |
| |
| |
| .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%; |
| } |
| |
| |
| .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); |
| } |
| |
| |
| .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); |
| } |
| |
| |
| .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; |
| } |
| |
| |
| |
| |
| |
| .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; |
| } |
| |
| |
| .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> |
|
|