thibaud frere commited on
Commit
9a962ce
·
1 Parent(s): d1dcd5a

update trackio design

Browse files
app/astro.config.mjs CHANGED
@@ -1,5 +1,6 @@
1
  import { defineConfig } from 'astro/config';
2
  import mdx from '@astrojs/mdx';
 
3
  import mermaid from 'astro-mermaid';
4
  import compressor from 'astro-compressor';
5
  import remarkMath from 'remark-math';
@@ -25,6 +26,7 @@ export default defineConfig({
25
  integrations: [
26
  mermaid({ theme: 'forest', autoTheme: true }),
27
  mdx(),
 
28
  // Precompress output with Gzip only (Brotli disabled due to server module mismatch)
29
  compressor({ brotli: false, gzip: true })
30
  ],
 
1
  import { defineConfig } from 'astro/config';
2
  import mdx from '@astrojs/mdx';
3
+ import svelte from '@astrojs/svelte';
4
  import mermaid from 'astro-mermaid';
5
  import compressor from 'astro-compressor';
6
  import remarkMath from 'remark-math';
 
26
  integrations: [
27
  mermaid({ theme: 'forest', autoTheme: true }),
28
  mdx(),
29
+ svelte(),
30
  // Precompress output with Gzip only (Brotli disabled due to server module mismatch)
31
  compressor({ brotli: false, gzip: true })
32
  ],
app/package-lock.json CHANGED
Binary files a/app/package-lock.json and b/app/package-lock.json differ
 
app/package.json CHANGED
Binary files a/app/package.json and b/app/package.json differ
 
app/src/components/TrackioWrapper.astro ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ // TrackioWrapper.astro
3
+ import Trackio from './trackio/Trackio.svelte';
4
+ ---
5
+
6
+ <!-- Ensure Roboto Mono is loaded for Oblivion theme -->
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap" rel="stylesheet">
10
+
11
+ <div class="trackio-wrapper">
12
+ <div class="trackio-controls">
13
+ <div class="controls-left">
14
+ <div class="theme-selector">
15
+ <label for="theme-select">Theme</label>
16
+ <select id="theme-select" class="theme-select">
17
+ <option value="classic">Classic</option>
18
+ <option value="oblivion">Oblivion</option>
19
+ </select>
20
+ </div>
21
+ <div class="scale-controls">
22
+ <label>
23
+ <input type="checkbox" id="log-scale-x">
24
+ Log Scale X
25
+ </label>
26
+ <label>
27
+ <input type="checkbox" id="smooth-data">
28
+ Smooth
29
+ </label>
30
+ </div>
31
+ </div>
32
+ <button class="button button--ghost" type="button" id="randomize-btn">
33
+ Randomize Data
34
+ </button>
35
+ </div>
36
+
37
+ <div class="trackio-container">
38
+ <Trackio client:load variant="classic" />
39
+ </div>
40
+ </div>
41
+
42
+ <script>
43
+ document.addEventListener('DOMContentLoaded', async () => {
44
+ const themeSelect = document.getElementById('theme-select');
45
+ const randomizeBtn = document.getElementById('randomize-btn');
46
+ const logScaleXCheckbox = document.getElementById('log-scale-x');
47
+ const smoothDataCheckbox = document.getElementById('smooth-data');
48
+ const trackioContainer = document.querySelector('.trackio-container');
49
+
50
+ if (!themeSelect || !randomizeBtn || !logScaleXCheckbox || !smoothDataCheckbox || !trackioContainer) return;
51
+
52
+ // Import the store function
53
+ const { triggerJitter } = await import('./trackio/store.js');
54
+
55
+ // Theme change handler
56
+ themeSelect.addEventListener('change', (e) => {
57
+ const target = e.target;
58
+ if (!target || !('value' in target)) return;
59
+
60
+ const newVariant = target.value;
61
+ console.log(`Theme changed to: ${newVariant}`); // Debug log
62
+
63
+ // Find the trackio element and call setTheme on the Svelte instance
64
+ const trackioEl = trackioContainer.querySelector('.trackio');
65
+ if (trackioEl && trackioEl.__trackioInstance) {
66
+ console.log('Calling setTheme on Trackio instance'); // Debug log
67
+ trackioEl.__trackioInstance.setTheme(newVariant);
68
+ } else {
69
+ // Fallback: just update CSS classes
70
+ console.log('No Trackio instance found, updating CSS classes only'); // Debug log
71
+ if (trackioEl) {
72
+ trackioEl.classList.remove('theme--classic', 'theme--oblivion');
73
+ trackioEl.classList.add(`theme--${newVariant}`);
74
+ }
75
+ }
76
+ });
77
+
78
+ // Log scale X change handler
79
+ logScaleXCheckbox.addEventListener('change', (e) => {
80
+ const target = e.target;
81
+ if (!target || !('checked' in target)) return;
82
+
83
+ const isLogScale = target.checked;
84
+ console.log(`Log scale X changed to: ${isLogScale}`); // Debug log
85
+
86
+ // Find the trackio element and call setLogScaleX on the Svelte instance
87
+ const trackioEl = trackioContainer.querySelector('.trackio');
88
+ if (trackioEl && trackioEl.__trackioInstance) {
89
+ console.log('Calling setLogScaleX on Trackio instance'); // Debug log
90
+ trackioEl.__trackioInstance.setLogScaleX(isLogScale);
91
+ } else {
92
+ console.log('Trackio instance not found for log scale change');
93
+ }
94
+ });
95
+
96
+ // Smooth data change handler
97
+ smoothDataCheckbox.addEventListener('change', (e) => {
98
+ const target = e.target;
99
+ if (!target || !('checked' in target)) return;
100
+
101
+ const isSmooth = target.checked;
102
+ console.log(`Smooth data changed to: ${isSmooth}`); // Debug log
103
+
104
+ // Find the trackio element and call setSmoothing on the Svelte instance
105
+ const trackioEl = trackioContainer.querySelector('.trackio');
106
+ if (trackioEl && trackioEl.__trackioInstance) {
107
+ console.log('Calling setSmoothing on Trackio instance'); // Debug log
108
+ trackioEl.__trackioInstance.setSmoothing(isSmooth);
109
+ } else {
110
+ console.log('Trackio instance not found for smooth change');
111
+ }
112
+ });
113
+
114
+ // Randomize data handler - now uses the store
115
+ randomizeBtn.addEventListener('click', () => {
116
+ console.log('Randomize button clicked - triggering jitter via store'); // Debug log
117
+
118
+ // Add vibration animation
119
+ randomizeBtn.classList.add('vibrating');
120
+ setTimeout(() => {
121
+ randomizeBtn.classList.remove('vibrating');
122
+ }, 600);
123
+
124
+ // Test direct window approach as well
125
+ if (window.trackioInstance && typeof window.trackioInstance.jitterData === 'function') {
126
+ console.log('Found window.trackioInstance, calling jitterData directly'); // Debug log
127
+ window.trackioInstance.jitterData();
128
+ } else {
129
+ console.log('No window.trackioInstance found, using store trigger'); // Debug log
130
+ triggerJitter();
131
+ }
132
+ });
133
+ });
134
+ </script>
135
+
136
+ <style>
137
+ .trackio-wrapper {
138
+ width: 100%;
139
+ margin: 0px 0 20px 0;
140
+ }
141
+
142
+ .trackio-controls {
143
+ display: flex;
144
+ justify-content: space-between;
145
+ align-items: center;
146
+ margin-bottom: 16px;
147
+ padding: 12px 0px;
148
+ border-bottom: 1px solid var(--border-color);
149
+ gap: 16px;
150
+ flex-wrap: nowrap;
151
+ }
152
+
153
+ .controls-left {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 24px;
157
+ flex-wrap: wrap;
158
+ }
159
+
160
+ .btn-randomize {
161
+ display: inline-flex;
162
+ align-items: center;
163
+ gap: 6px;
164
+ padding: 8px 16px;
165
+ background: var(--accent-color, #007acc);
166
+ color: white;
167
+ border: none;
168
+ border-radius: 6px;
169
+ font-size: 14px;
170
+ font-weight: 500;
171
+ cursor: pointer;
172
+ transition: all 0.15s ease;
173
+ }
174
+
175
+ .btn-randomize:hover {
176
+ background: var(--accent-hover, #005a9e);
177
+ transform: translateY(-1px);
178
+ }
179
+
180
+ .btn-randomize:active {
181
+ transform: translateY(0);
182
+ }
183
+
184
+ .theme-selector {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ font-size: 14px;
189
+ flex-shrink: 0;
190
+ white-space: nowrap;
191
+ }
192
+
193
+ .theme-selector label {
194
+ font-weight: 500;
195
+ color: var(--text-color);
196
+ }
197
+
198
+ .theme-select {
199
+ padding: 6px 12px;
200
+ border: 1px solid var(--border-color);
201
+ border-radius: 4px;
202
+ background: var(--input-bg, var(--surface-bg));
203
+ color: var(--text-color);
204
+ font-size: 14px;
205
+ cursor: pointer;
206
+ transition: border-color 0.15s ease;
207
+ }
208
+
209
+ .theme-select:focus {
210
+ outline: none;
211
+ border-color: var(--accent-color, #007acc);
212
+ }
213
+
214
+ .scale-controls {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 16px;
218
+ flex-shrink: 0;
219
+ white-space: nowrap;
220
+ }
221
+
222
+ /* Animation de vibration pour le bouton */
223
+ @keyframes vibrate {
224
+ 0% { transform: translateX(0); }
225
+ 10% { transform: translateX(-2px) rotate(-1deg); }
226
+ 20% { transform: translateX(2px) rotate(1deg); }
227
+ 30% { transform: translateX(-2px) rotate(-1deg); }
228
+ 40% { transform: translateX(2px) rotate(1deg); }
229
+ 50% { transform: translateX(-1px) rotate(-0.5deg); }
230
+ 60% { transform: translateX(1px) rotate(0.5deg); }
231
+ 70% { transform: translateX(-1px) rotate(-0.5deg); }
232
+ 80% { transform: translateX(1px) rotate(0.5deg); }
233
+ 90% { transform: translateX(-0.5px) rotate(-0.25deg); }
234
+ 100% { transform: translateX(0) rotate(0); }
235
+ }
236
+
237
+ .button.vibrating {
238
+ animation: vibrate 0.6s ease-in-out;
239
+ }
240
+
241
+ .trackio-container {
242
+ width: 100%;
243
+ margin-top: 40px;
244
+ }
245
+
246
+ @media (max-width: 768px) {
247
+ .trackio-controls {
248
+ flex-direction: column;
249
+ align-items: stretch;
250
+ gap: 12px;
251
+ }
252
+
253
+ .controls-left {
254
+ flex-direction: column;
255
+ align-items: stretch;
256
+ gap: 12px;
257
+ }
258
+
259
+ .theme-selector {
260
+ justify-content: space-between;
261
+ }
262
+
263
+ .scale-controls {
264
+ justify-content: space-between;
265
+ }
266
+ }
267
+ </style>
app/src/components/trackio/Cell.svelte ADDED
@@ -0,0 +1,902 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import * as d3 from 'd3';
3
+ import { onMount, onDestroy } from 'svelte';
4
+ import Tooltip from './Tooltip.svelte';
5
+ import { formatAbbrev, formatLogTick, generateSmartTicks, generateLogTicks } from './chart-utils.js';
6
+ export let metricKey;
7
+ export let titleText;
8
+ export let wide = false;
9
+ export let variant = 'classic';
10
+ export let normalizeLoss = true;
11
+ export let logScaleX = false;
12
+ export let smoothing = false;
13
+ export let metricData = {}; // { run -> [{step,value}] } - smoothed data
14
+ export let rawMetricData = {}; // { run -> [{step,value}] } - original data for background when smoothing
15
+ export let colorForRun = (name)=> '#999';
16
+ export let hostEl = null;
17
+
18
+ let root; let body;
19
+ let svg, gRoot, gGrid, gGridDots, gAxes, gAreas, gLines, gPoints, gHover;
20
+ let xScale, yScale, lineGen;
21
+ let tooltip, tipTarget;
22
+ let cleanup;
23
+ const MARGIN = { top: 10, right: 20, bottom: 46, left: 44 };
24
+
25
+ // Reactive rendering when variant or metricData changes
26
+ $: {
27
+ if (variant || metricData) {
28
+ // Add a small delay to ensure CSS variables are updated
29
+ setTimeout(() => render(), 10);
30
+ }
31
+ }
32
+
33
+ // Utility function to get transition duration based on fullscreen state
34
+ function getTransitionDuration(normalDuration = 160) {
35
+ const fullscreenOverlay = root?.closest('.trackio-fullscreen-overlay');
36
+ return (fullscreenOverlay && fullscreenOverlay.classList.contains('transitioning')) ? 0 : normalDuration;
37
+ }
38
+
39
+ function ensureSvg(){
40
+ if (svg || !body) return;
41
+ const d3body = d3.select(body);
42
+ svg = d3body.append('svg').attr('width','100%').style('display','block');
43
+ gRoot = svg.append('g');
44
+ gGrid = gRoot.append('g').attr('class','grid');
45
+ gGridDots = gRoot.append('g').attr('class','grid-dots');
46
+ gAxes = gRoot.append('g').attr('class','axes');
47
+ gAreas = gRoot.append('g').attr('class','areas');
48
+ gLines = gRoot.append('g').attr('class','lines');
49
+ gPoints = gRoot.append('g').attr('class','points');
50
+ gHover = gRoot.append('g').attr('class','hover');
51
+ // Initialize scales - X scale will be updated based on logScaleX prop
52
+ xScale = logScaleX ? d3.scaleLog() : d3.scaleLinear();
53
+ yScale = d3.scaleLinear();
54
+ lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
55
+ // Create tooltip container in the trackio parent to inherit CSS variables
56
+ const trackioEl = root.closest('.trackio') || root;
57
+ tipTarget = document.createElement('div');
58
+ tipTarget.className = 'tip-host';
59
+ trackioEl.appendChild(tipTarget);
60
+ tooltip = new Tooltip({ target: tipTarget, props: { visible:false, x:-9999, y:-9999, title:'', subtitle:'', entries:[] } });
61
+ }
62
+
63
+
64
+ function updateLayout(hoverSteps, themeVars = {}){
65
+ const { axisStroke = 'var(--trackio-chart-axis-stroke)', axisText = 'var(--trackio-chart-axis-text)', gridStroke = 'var(--trackio-chart-grid-stroke)' } = themeVars;
66
+
67
+ // Get font-family from computed style
68
+ const computedStyle = getComputedStyle(root);
69
+ const fontFamily = computedStyle.getPropertyValue('--trackio-font-family').trim() || 'ui-monospace, SFMono-Regular, Menlo, monospace';
70
+ const rect = root.getBoundingClientRect();
71
+ let width = Math.max(1, Math.round(rect && rect.width ? rect.width : (root.clientWidth || 800)));
72
+
73
+ // Check if we're in fullscreen mode
74
+ const isFullscreen = root.closest('.trackio-fullscreen-modal');
75
+ let height;
76
+
77
+ if (isFullscreen) {
78
+ // Use available height minus header in fullscreen
79
+ // Use the body element's actual size
80
+ const bodyElement = body.parentElement; // cell-body
81
+ if (bodyElement) {
82
+ const bodyRect = bodyElement.getBoundingClientRect();
83
+ height = Math.max(400, Math.floor(bodyRect.height - 20)); // Small margin
84
+ } else {
85
+ height = 600; // Fallback
86
+ }
87
+ } else {
88
+ height = Number(root.getAttribute('data-height')) || 180;
89
+ }
90
+ if (isFullscreen) {
91
+ // In fullscreen, stretch to fill completely (no aspect ratio preservation)
92
+ svg.attr('width', '100%').attr('height', '100%').attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','none');
93
+ } else {
94
+ // Normal mode, preserve aspect ratio
95
+ svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
96
+ }
97
+ const innerWidth = width - MARGIN.left - MARGIN.right; const innerHeight = height - MARGIN.top - MARGIN.bottom;
98
+ gRoot.attr('transform', `translate(${MARGIN.left},${MARGIN.top})`);
99
+ xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);
100
+
101
+ gAxes.selectAll('*').remove();
102
+
103
+ const minXTicks = 5;
104
+ const maxXTicks = Math.max(minXTicks, Math.min(12, Math.floor(innerWidth / 70)));
105
+ let xTicksForced = [];
106
+
107
+ let logTickData = null;
108
+ if (logScaleX) {
109
+ // Use improved logarithmic tick generation
110
+ logTickData = generateLogTicks(hoverSteps, minXTicks, maxXTicks, innerWidth, xScale);
111
+ xTicksForced = logTickData.major;
112
+ } else if (Array.isArray(hoverSteps) && hoverSteps.length) {
113
+ const tickIndices = generateSmartTicks(hoverSteps, minXTicks, maxXTicks, innerWidth);
114
+ xTicksForced = tickIndices;
115
+ } else {
116
+ // Fallback for continuous scales
117
+ const makeTicks = (scale, approx) => {
118
+ const arr = scale.ticks(approx);
119
+ const dom = scale.domain();
120
+ if (arr.length === 0 || arr[0] !== dom[0]) arr.unshift(dom[0]);
121
+ if (arr[arr.length-1] !== dom[dom.length-1]) arr.push(dom[dom.length-1]);
122
+ return Array.from(new Set(arr));
123
+ };
124
+ xTicksForced = makeTicks(xScale, maxXTicks);
125
+ }
126
+ const maxYTicks = Math.max(5, Math.min(6, Math.floor(innerHeight / 60)));
127
+ const yCount = maxYTicks; const yDom = yScale.domain();
128
+ const yTicksForced = (yCount <= 2) ? [yDom[0], yDom[1]] : Array.from({length:yCount}, (_,i)=> yDom[0] + ((yDom[1]-yDom[0])*(i/(yCount-1))));
129
+
130
+ gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(xScale).tickValues(xTicksForced).tickFormat((val)=>{
131
+ // For log scale, val is the actual step value; for linear scale, val might be an index
132
+ const displayVal = logScaleX ? val : (Array.isArray(hoverSteps) && hoverSteps[val] != null ? hoverSteps[val] : val);
133
+ return logScaleX ? formatLogTick(displayVal, true) : formatAbbrev(displayVal);
134
+ }))
135
+ .call(g=>{
136
+ g.selectAll('path, line').style('stroke', 'var(--trackio-chart-axis-stroke)');
137
+ g.selectAll('text').style('fill', 'var(--trackio-chart-axis-text)').style('font-size','11px').style('font-family', fontFamily)
138
+ .style('font-weight', d => {
139
+ // Make powers of 10 bolder in log scale
140
+ if (!logScaleX) return 'normal';
141
+ const log10 = Math.log10(Math.abs(d));
142
+ const isPowerOf10 = Math.abs(log10 % 1) < 0.01;
143
+ return isPowerOf10 ? '600' : 'normal';
144
+ });
145
+ });
146
+ gAxes.append('g').call(d3.axisLeft(yScale).tickValues(yTicksForced).tickFormat((v)=>formatAbbrev(v)))
147
+ .call(g=>{
148
+ g.selectAll('path, line').style('stroke', 'var(--trackio-chart-axis-stroke)');
149
+ g.selectAll('text').style('fill', 'var(--trackio-chart-axis-text)').style('font-size','11px').style('font-family', fontFamily);
150
+ });
151
+
152
+ // Add minor ticks for logarithmic scale
153
+ if (logScaleX && logTickData && logTickData.minor.length > 0) {
154
+ gAxes.append('g').attr('class', 'minor-ticks').attr('transform', `translate(0,${innerHeight})`)
155
+ .selectAll('line.minor-tick')
156
+ .data(logTickData.minor)
157
+ .join('line')
158
+ .attr('class', 'minor-tick')
159
+ .attr('x1', d => xScale(d))
160
+ .attr('x2', d => xScale(d))
161
+ .attr('y1', 0)
162
+ .attr('y2', 4) // Smaller than major ticks
163
+ .style('stroke', 'var(--trackio-chart-axis-stroke)')
164
+ .style('stroke-opacity', 0.4)
165
+ .style('stroke-width', 0.5);
166
+ }
167
+
168
+
169
+ // Grid rendering is now handled in render() function based on theme
170
+ const labelY = innerHeight + Math.max(20, Math.min(36, MARGIN.bottom - 12));
171
+
172
+ // Main "Steps" label (normal text)
173
+ // Add (log) to Steps text when log scale is enabled
174
+ const stepsText = logScaleX ? 'Steps (log)' : 'Steps';
175
+
176
+ gAxes.append('text')
177
+ .attr('class','x-axis-label')
178
+ .attr('x', innerWidth/2)
179
+ .attr('y', labelY)
180
+ .style('fill', 'var(--trackio-chart-axis-text)')
181
+ .attr('text-anchor','middle')
182
+ .style('font-size','9px')
183
+ .style('opacity','.9')
184
+ .style('letter-spacing','.5px')
185
+ .style('text-transform','uppercase')
186
+ .style('font-weight','500')
187
+ .style('font-family', fontFamily)
188
+ .text(stepsText);
189
+
190
+ return { innerWidth, innerHeight, xTicksForced, yTicksForced };
191
+ }
192
+
193
+ function render(){
194
+ ensureSvg();
195
+ if (!svg || !gRoot) return; // Wait for SVG to be ready
196
+ const runs = Object.keys(metricData||{});
197
+ const hasAny = runs.some(r => (metricData[r]||[]).length > 0);
198
+ if (!hasAny) { gRoot.style('display','none'); return; } gRoot.style('display', null);
199
+
200
+ // Get theme variables from CSS - try parent element if root doesn't have the variables
201
+ let computedStyle = getComputedStyle(root);
202
+ let gridType = computedStyle.getPropertyValue('--trackio-chart-grid-type').trim().replace(/['"]/g, '');
203
+
204
+ // If not found on root, try parent trackio element
205
+ if (!gridType) {
206
+ const trackioEl = root?.closest('.trackio');
207
+ if (trackioEl) {
208
+ computedStyle = getComputedStyle(trackioEl);
209
+ gridType = computedStyle.getPropertyValue('--trackio-chart-grid-type').trim().replace(/['"]/g, '');
210
+ }
211
+ }
212
+
213
+ const axisStroke = computedStyle.getPropertyValue('--trackio-chart-axis-stroke').trim();
214
+ const axisText = computedStyle.getPropertyValue('--trackio-chart-axis-text').trim();
215
+ const gridStroke = computedStyle.getPropertyValue('--trackio-chart-grid-stroke').trim();
216
+ const gridOpacity = computedStyle.getPropertyValue('--trackio-chart-grid-opacity').trim();
217
+
218
+ console.log('🎯 Cell render - variant:', variant, 'gridType:', gridType, 'trackio classes:', root?.closest('.trackio')?.className);
219
+
220
+ let minStep=Infinity, maxStep=-Infinity, minVal=Infinity, maxVal=-Infinity;
221
+ runs.forEach(r => { (metricData[r]||[]).forEach(pt => { minStep=Math.min(minStep, pt.step); maxStep=Math.max(maxStep, pt.step); minVal=Math.min(minVal, pt.value); maxVal=Math.max(maxVal, pt.value); }); });
222
+ const isAccuracy = /accuracy/i.test(metricKey); const isLoss = /loss/i.test(metricKey);
223
+ if (isAccuracy) yScale.domain([0,1]).nice(); else if (isLoss && normalizeLoss) yScale.domain([0,1]).nice(); else yScale.domain([minVal, maxVal]).nice();
224
+ const stepSet = new Set(); runs.forEach(r => (metricData[r]||[]).forEach(v => stepSet.add(v.step)));
225
+ const hoverSteps = Array.from(stepSet).sort((a,b)=>a-b);
226
+
227
+ // Update X scale based on logScaleX prop
228
+ xScale = logScaleX ? d3.scaleLog() : d3.scaleLinear();
229
+
230
+ let stepIndex = null; // Declare stepIndex in the proper scope
231
+
232
+ if (logScaleX) {
233
+ // For log scale, use actual step values (must be > 0)
234
+ const minStep = Math.max(1, Math.min(...hoverSteps));
235
+ const maxStep = Math.max(...hoverSteps);
236
+ xScale.domain([minStep, maxStep]);
237
+ lineGen.x(d => xScale(d.step));
238
+ } else {
239
+ // For linear scale, use indices as before
240
+ stepIndex = new Map(hoverSteps.map((s,i)=>[s,i]));
241
+ xScale.domain([0, Math.max(0, hoverSteps.length - 1)]);
242
+ lineGen.x(d => xScale(stepIndex.get(d.step)));
243
+ }
244
+ const normalizeY = (v) => (isLoss && normalizeLoss ? ((maxVal > minVal) ? (v - minVal) / (maxVal - minVal) : 0) : v); lineGen.y(d => yScale(normalizeY(d.value)));
245
+ const { innerWidth, innerHeight, xTicksForced, yTicksForced } = updateLayout(hoverSteps, { axisStroke, axisText, gridStroke });
246
+
247
+ // Conditional grid rendering based on theme
248
+ // Force detection based on variant prop if CSS variables fail
249
+ const shouldUseDots = (gridType === 'dots') || (variant === 'oblivion');
250
+
251
+ if (shouldUseDots) {
252
+ // Oblivion-style: Grid as dots at intersections
253
+ gGrid.selectAll('*').remove(); // Clear line grid
254
+ gGridDots.selectAll('*').remove();
255
+ const gridPoints = [];
256
+ const yMin = yScale.domain()[0];
257
+ const desiredCols = 24;
258
+ const xGridStride = Math.max(1, Math.ceil(hoverSteps.length/desiredCols));
259
+ const xGridIdx = [];
260
+ for (let idx = 0; idx < hoverSteps.length; idx += xGridStride) xGridIdx.push(idx);
261
+ if (xGridIdx[xGridIdx.length-1] !== hoverSteps.length-1) xGridIdx.push(hoverSteps.length-1);
262
+ xGridIdx.forEach(i => {
263
+ yTicksForced.forEach(t => {
264
+ if (i !== 0 && (yMin == null || t !== yMin)) gridPoints.push({ sx: i, ty: t });
265
+ });
266
+ });
267
+ gGridDots.selectAll('circle.grid-dot')
268
+ .data(gridPoints)
269
+ .join('circle')
270
+ .attr('class', 'grid-dot')
271
+ .attr('cx', d => xScale(d.sx))
272
+ .attr('cy', d => yScale(d.ty))
273
+ .attr('r', 1.25)
274
+ .style('fill', 'var(--trackio-chart-grid-stroke)')
275
+ .style('fill-opacity', 'var(--trackio-chart-grid-opacity)');
276
+ } else {
277
+ // Classic-style: Grid as lines
278
+ gGridDots.selectAll('*').remove(); // Clear dot grid
279
+ gGrid.selectAll('*').remove();
280
+ // Horizontal grid lines
281
+ gGrid.selectAll('line.horizontal')
282
+ .data(yTicksForced)
283
+ .join('line')
284
+ .attr('class', 'horizontal')
285
+ .attr('x1', 0).attr('x2', innerWidth)
286
+ .attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
287
+ .style('stroke', 'var(--trackio-chart-grid-stroke)')
288
+ .style('stroke-opacity', 'var(--trackio-chart-grid-opacity)')
289
+ .attr('stroke-width', 1);
290
+ // Vertical grid lines
291
+ gGrid.selectAll('line.vertical')
292
+ .data(xTicksForced)
293
+ .join('line')
294
+ .attr('class', 'vertical')
295
+ .attr('x1', d => xScale(d)).attr('x2', d => xScale(d))
296
+ .attr('y1', 0).attr('y2', innerHeight)
297
+ .style('stroke', 'var(--trackio-chart-grid-stroke)')
298
+ .style('stroke-opacity', 'var(--trackio-chart-grid-opacity)')
299
+ .attr('stroke-width', 1);
300
+ }
301
+
302
+ const series = runs.map(r => ({ run:r, color: colorForRun(r), values: (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
303
+
304
+ // Draw background lines (original data) when smoothing is enabled
305
+ if (smoothing && rawMetricData && Object.keys(rawMetricData).length > 0) {
306
+ const rawSeries = runs.map(r => ({ run:r, color: colorForRun(r), values: (rawMetricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
307
+ const rawPaths = gLines.selectAll('path.raw-line').data(rawSeries, d=>d.run + '-raw');
308
+ rawPaths.enter().append('path').attr('class','raw-line').attr('data-run', d=>d.run).attr('fill','none').attr('stroke-width',1).attr('opacity',0.9).attr('stroke', d=>d.color).style('pointer-events','none').attr('d', d=> lineGen(d.values));
309
+ rawPaths.transition().duration(getTransitionDuration(160)).attr('stroke', d=>d.color).attr('opacity',0.4).attr('d', d=> lineGen(d.values));
310
+ rawPaths.exit().remove();
311
+ } else {
312
+ // Remove raw lines when smoothing is disabled
313
+ gLines.selectAll('path.raw-line').remove();
314
+ }
315
+
316
+ // Draw main lines (smoothed or normal data)
317
+ const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
318
+ paths.enter().append('path').attr('class','run-line').attr('data-run', d=>d.run).attr('fill','none').attr('stroke-width',1.5).attr('opacity',0.9).attr('stroke', d=>d.color).style('pointer-events','none').attr('d', d=> lineGen(d.values));
319
+ paths.transition().duration(getTransitionDuration(160)).attr('stroke', d=>d.color).attr('opacity',0.9).attr('d', d=> lineGen(d.values));
320
+ paths.exit().remove();
321
+ const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
322
+ const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
323
+ ptsSel.enter().append('circle').attr('class','pt').attr('data-run', d=>d.run).attr('r',0).attr('fill', d=>d.color).attr('fill-opacity',0.6).attr('stroke','none').style('pointer-events','none')
324
+ .attr('cx', d=> logScaleX ? xScale(d.step) : xScale(stepIndex.get(d.step)))
325
+ .attr('cy', d=> yScale(normalizeY(d.value)))
326
+ .merge(ptsSel)
327
+ .attr('cx', d=> logScaleX ? xScale(d.step) : xScale(stepIndex.get(d.step)))
328
+ .attr('cy', d=> yScale(normalizeY(d.value)));
329
+ ptsSel.exit().remove();
330
+
331
+ gHover.selectAll('*').remove(); const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight).style('pointer-events','all'); const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity',0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none').style('pointer-events','none'); let hideTipTimer=null;
332
+ function onMove(ev){
333
+ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer=null; }
334
+ const [mx,my]=d3.pointer(ev, overlay.node());
335
+
336
+ let nearest, xpx;
337
+ if (logScaleX) {
338
+ // For log scale, find the closest actual step value
339
+ const mouseStepValue = xScale.invert(mx);
340
+ let minDist = Infinity;
341
+ let closestStep = hoverSteps[0];
342
+ hoverSteps.forEach(step => {
343
+ const dist = Math.abs(Math.log(step) - Math.log(mouseStepValue));
344
+ if (dist < minDist) {
345
+ minDist = dist;
346
+ closestStep = step;
347
+ }
348
+ });
349
+ nearest = closestStep;
350
+ xpx = xScale(nearest);
351
+ } else {
352
+ // For linear scale, use index-based approach as before
353
+ const idx = Math.round(Math.max(0, Math.min(hoverSteps.length-1, xScale.invert(mx))));
354
+ nearest = hoverSteps[idx];
355
+ xpx = xScale(idx);
356
+ }
357
+
358
+ hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
359
+ try { hostEl && hostEl.dispatchEvent(new CustomEvent('trackio-hover-step', { detail: { step: nearest } })); } catch(_) {}
360
+ const entries = series.map(s=>{ const m = new Map(s.values.map(v=>[v.step, v])); const pt = m.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null).sort((a,b)=> a.pt.value - b.pt.value);
361
+ const fmt=(vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4));
362
+
363
+ // Calculate position relative to trackio container
364
+ const trackioEl = root.closest('.trackio') || root;
365
+ const rootRect = root.getBoundingClientRect();
366
+ const trackioRect = trackioEl.getBoundingClientRect();
367
+ const relativeX = rootRect.left - trackioRect.left + mx + 12 + MARGIN.left;
368
+ const relativeY = rootRect.top - trackioRect.top + my + 12 + MARGIN.top;
369
+ tooltip.$set({ visible:true, x: Math.round(relativeX), y: Math.round(relativeY), title:`Step ${formatAbbrev(nearest)}`, subtitle: titleText, entries: entries.map(e=> ({ color:e.color, name:e.run, valueText: fmt(e.pt.value) })) });
370
+ try { gPoints.selectAll('circle.pt').transition().duration(getTransitionDuration(120)).ease(d3.easeCubicOut).attr('r', d => (d && d.step === nearest ? 4 : 0)); } catch(_) {}
371
+ }
372
+ function onLeave(){ hideTipTimer = setTimeout(()=>{ tooltip.$set({ visible:false, x:-9999, y:-9999 }); hoverLine.style('display','none'); try { hostEl && hostEl.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_) {} try { gPoints.selectAll('circle.pt').transition().duration(getTransitionDuration(120)).ease(d3.easeCubicOut).attr('r', 0); } catch(_) {} }, 80); }
373
+ overlay.on('mousemove', onMove).on('mouseleave', onLeave);
374
+
375
+ // External hover
376
+ root.__showExternalStep = (stepVal) => {
377
+ if (stepVal==null) {
378
+ hoverLine.style('display','none');
379
+ try { gPoints.selectAll('circle.pt').attr('r',0); } catch(_) {}
380
+ return;
381
+ }
382
+ let xpx;
383
+ if (logScaleX) {
384
+ xpx = xScale(stepVal);
385
+ } else {
386
+ const idx = stepIndex ? stepIndex.get(stepVal) : null;
387
+ if (idx == null) { hoverLine.style('display','none'); return; }
388
+ xpx = xScale(idx);
389
+ }
390
+ hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
391
+ try { gPoints.selectAll('circle.pt').attr('r', d => (d && d.step === stepVal ? 4 : 0)); } catch(_) {}
392
+ };
393
+ root.__clearExternalStep = () => { hoverLine.style('display','none'); try { gPoints.selectAll('circle.pt').attr('r',0); } catch(_) {} };
394
+ if (!root.__syncAttached && hostEl) { hostEl.addEventListener('trackio-hover-step', (ev)=>{ const d=ev&&ev.detail; if (!d) return; root.__showExternalStep && root.__showExternalStep(d.step); }); hostEl.addEventListener('trackio-hover-clear', ()=>{ root.__clearExternalStep && root.__clearExternalStep(); }); root.__syncAttached = true; }
395
+ }
396
+
397
+ function schedule(){ try { render(); } catch(_) {} }
398
+ onMount(()=>{ schedule(); const ro = (window.ResizeObserver ? new ResizeObserver(()=> schedule()) : null); ro && ro.observe(root); cleanup = ()=>{ ro && ro.disconnect(); }; });
399
+ onDestroy(()=>{ cleanup && cleanup(); });
400
+
401
+ // Re-render when inputs change so tooltip/overlay are available once data arrives
402
+ $: {
403
+ metricData;
404
+ rawMetricData;
405
+ normalizeLoss;
406
+ variant;
407
+ logScaleX;
408
+ smoothing;
409
+ colorForRun;
410
+ // Debug log
411
+ if (typeof window !== 'undefined') {
412
+ console.log(`Cell re-rendering for ${metricKey}, variant: ${variant}, logScaleX: ${logScaleX}, smoothing: ${smoothing}`);
413
+ }
414
+ schedule();
415
+ }
416
+
417
+ // Fullscreen functionality
418
+ function openFullscreen() {
419
+ if (!root) return;
420
+
421
+ // Create overlay if it doesn't exist
422
+ let overlay = document.querySelector('.trackio-fullscreen-overlay');
423
+ if (!overlay) {
424
+ overlay = document.createElement('div');
425
+ overlay.className = 'trackio-fullscreen-overlay';
426
+
427
+ const modal = document.createElement('div');
428
+ modal.className = 'trackio-fullscreen-modal';
429
+
430
+ const closeBtn = document.createElement('button');
431
+ closeBtn.className = 'trackio-fullscreen-close';
432
+ closeBtn.innerHTML = '×';
433
+ closeBtn.title = 'Fermer';
434
+
435
+ overlay.appendChild(modal);
436
+ overlay.appendChild(closeBtn);
437
+ document.body.appendChild(overlay);
438
+
439
+ // Close handlers
440
+ const closeModal = () => {
441
+ const cellInModal = modal.querySelector('.cell');
442
+ if (!cellInModal || !cellInModal.__originalParent) {
443
+ overlay.classList.remove('is-open');
444
+ return;
445
+ }
446
+
447
+ // FLIP animation: animate back to original position
448
+ const currentRect = cellInModal.getBoundingClientRect();
449
+ const targetRect = cellInModal.__placeholder.getBoundingClientRect();
450
+
451
+ const deltaX = targetRect.left - currentRect.left;
452
+ const deltaY = targetRect.top - currentRect.top;
453
+ const scaleX = targetRect.width / currentRect.width;
454
+ const scaleY = targetRect.height / currentRect.height;
455
+
456
+ // Animate back
457
+ overlay.classList.add('transitioning'); // Disable D3 animations during close
458
+ cellInModal.style.transformOrigin = 'top left';
459
+ cellInModal.style.transition = 'transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)';
460
+ cellInModal.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
461
+
462
+ overlay.classList.remove('is-open');
463
+
464
+ setTimeout(() => {
465
+ // Move cell back to original position
466
+ if (cellInModal.__placeholder && cellInModal.__originalParent) {
467
+ cellInModal.__originalParent.insertBefore(cellInModal, cellInModal.__placeholder);
468
+ cellInModal.__placeholder.remove();
469
+ }
470
+
471
+ // Reset styles
472
+ cellInModal.style.transform = '';
473
+ cellInModal.style.transition = '';
474
+ cellInModal.style.transformOrigin = '';
475
+
476
+ // Clean up references
477
+ delete cellInModal.__originalParent;
478
+ delete cellInModal.__placeholder;
479
+
480
+ // Re-render to fix any layout issues
481
+ overlay.classList.remove('transitioning'); // Re-enable animations
482
+ schedule();
483
+ }, 300);
484
+ };
485
+
486
+ closeBtn.addEventListener('click', closeModal);
487
+ overlay.addEventListener('click', (e) => {
488
+ if (e.target === overlay) closeModal();
489
+ });
490
+
491
+ // ESC key handler
492
+ const handleEsc = (e) => {
493
+ if (e.key === 'Escape' && overlay.classList.contains('is-open')) {
494
+ closeModal();
495
+ }
496
+ };
497
+ document.addEventListener('keydown', handleEsc);
498
+
499
+ overlay.__closeModal = closeModal;
500
+ overlay.__handleEsc = handleEsc;
501
+ }
502
+
503
+ const modal = overlay.querySelector('.trackio-fullscreen-modal');
504
+
505
+ // Close any existing modal
506
+ if (overlay.classList.contains('is-open')) {
507
+ overlay.__closeModal();
508
+ return;
509
+ }
510
+
511
+ // FLIP animation: First - record initial position
512
+ const initialRect = root.getBoundingClientRect();
513
+
514
+ // Create placeholder
515
+ const placeholder = document.createElement('div');
516
+ placeholder.style.width = root.offsetWidth + 'px';
517
+ placeholder.style.height = root.offsetHeight + 'px';
518
+ placeholder.style.visibility = 'hidden';
519
+
520
+ // Store references
521
+ root.__originalParent = root.parentNode;
522
+ root.__placeholder = placeholder;
523
+
524
+ // Move cell to modal
525
+ root.parentNode.insertBefore(placeholder, root);
526
+ modal.appendChild(root);
527
+
528
+ // Last - record final position
529
+ overlay.classList.add('is-open');
530
+ const finalRect = root.getBoundingClientRect();
531
+
532
+ // Invert - calculate the difference
533
+ const deltaX = initialRect.left - finalRect.left;
534
+ const deltaY = initialRect.top - finalRect.top;
535
+ const scaleX = initialRect.width / finalRect.width;
536
+ const scaleY = initialRect.height / finalRect.height;
537
+
538
+ // Set initial transform (inverted)
539
+ root.style.transformOrigin = 'top left';
540
+ root.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
541
+
542
+ // Play - animate to final position
543
+ overlay.classList.add('transitioning'); // Add class to suppress animations
544
+ requestAnimationFrame(() => {
545
+ root.style.transition = 'transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)';
546
+ root.style.transform = 'translate(0, 0) scale(1, 1)';
547
+
548
+ setTimeout(() => {
549
+ root.style.transition = '';
550
+ root.style.transform = '';
551
+ root.style.transformOrigin = '';
552
+
553
+ // Re-render chart at new size with a small delay to ensure layout is settled
554
+ setTimeout(() => {
555
+ overlay.classList.remove('transitioning'); // Re-enable animations
556
+ schedule();
557
+ }, 50);
558
+ }, 300);
559
+ });
560
+ }
561
+ </script>
562
+
563
+ <style>
564
+ /* =========================
565
+ CELL BASE STYLES
566
+ ========================= */
567
+
568
+ :global(.trackio .cell) {
569
+ border: 1px solid var(--trackio-cell-border);
570
+ border-radius: 10px;
571
+ background: var(--trackio-cell-background);
572
+ display: flex;
573
+ flex-direction: column;
574
+ position: relative;
575
+ }
576
+
577
+ /* Default cell background - hidden */
578
+ :global(.trackio .cell-bg) {
579
+ position: absolute;
580
+ inset: 10px;
581
+ pointer-events: none;
582
+ z-index: 1;
583
+ border-radius: 4px;
584
+ display: none;
585
+ }
586
+
587
+ /* Default cell corners - hidden */
588
+ :global(.trackio .cell-corners) {
589
+ position: absolute;
590
+ inset: 6px;
591
+ pointer-events: none;
592
+ z-index: 3;
593
+ display: none;
594
+ opacity: 0.85;
595
+ }
596
+
597
+ :global(.trackio .cell-inner) {
598
+ position: relative;
599
+ z-index: 2;
600
+ padding: 8px 12px 10px 10px;
601
+ display: flex;
602
+ flex-direction: column;
603
+ }
604
+
605
+ /* Oblivion theme: adjust inner padding to account for corners and gap */
606
+ :global(.trackio.theme--oblivion .cell-inner) {
607
+ padding: var(--trackio-oblivion-hud-corner-size, 8px) 12px 10px var(--trackio-oblivion-hud-gap, 10px);
608
+ }
609
+
610
+ /* Oblivion theme: show background and corners with proper styling */
611
+ :global(.trackio.theme--oblivion .cell-bg) {
612
+ display: block !important;
613
+ background:
614
+ radial-gradient(1200px 200px at 20% -10%, rgba(0,0,0,.05), transparent 80%),
615
+ radial-gradient(900px 200px at 80% 110%, rgba(0,0,0,.05), transparent 80%);
616
+ }
617
+
618
+ /* Dark mode: richer gradient for Oblivion */
619
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion .cell-bg) {
620
+ background:
621
+ radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, #ffffff 6.5%, transparent), transparent 80%),
622
+ radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, #ffffff 6%, transparent), transparent 80%),
623
+ linear-gradient(180deg, color-mix(in srgb, #ffffff 3.5%, transparent), transparent 45%);
624
+ }
625
+
626
+ :global(.trackio.theme--oblivion .cell-corners) {
627
+ display: block !important;
628
+ inset: 6px;
629
+ background:
630
+ linear-gradient(#000000, #000000) top left / 8px 1px no-repeat,
631
+ linear-gradient(#000000, #000000) top left / 1px 8px no-repeat,
632
+ linear-gradient(#000000, #000000) top right / 8px 1px no-repeat,
633
+ linear-gradient(#000000, #000000) top right / 1px 8px no-repeat,
634
+ linear-gradient(#000000, #000000) bottom left / 8px 1px no-repeat,
635
+ linear-gradient(#000000, #000000) bottom left / 1px 8px no-repeat,
636
+ linear-gradient(#000000, #000000) bottom right / 8px 1px no-repeat,
637
+ linear-gradient(#000000, #000000) bottom right / 1px 8px no-repeat;
638
+ opacity: 1;
639
+ z-index: 3;
640
+ }
641
+
642
+ /* Dark mode: bright corners for Oblivion */
643
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion .cell-corners) {
644
+ background:
645
+ linear-gradient(#ffffff, #ffffff) top left / 8px 1px no-repeat,
646
+ linear-gradient(#ffffff, #ffffff) top left / 1px 8px no-repeat,
647
+ linear-gradient(#ffffff, #ffffff) top right / 8px 1px no-repeat,
648
+ linear-gradient(#ffffff, #ffffff) top right / 1px 8px no-repeat,
649
+ linear-gradient(#ffffff, #ffffff) bottom left / 8px 1px no-repeat,
650
+ linear-gradient(#ffffff, #ffffff) bottom left / 1px 8px no-repeat,
651
+ linear-gradient(#ffffff, #ffffff) bottom right / 8px 1px no-repeat,
652
+ linear-gradient(#ffffff, #ffffff) bottom right / 1px 8px no-repeat;
653
+ }
654
+
655
+ /* Dark mode corners are handled automatically by CSS variables */
656
+
657
+ /* Oblivion theme: cell title with indicator dot */
658
+ :global(.trackio.theme--oblivion .cell-title) {
659
+ font-size: 12px;
660
+ font-weight: 800;
661
+ letter-spacing: 0.12em;
662
+ text-transform: uppercase;
663
+ color: var(--trackio-oblivion-primary);
664
+ display: flex;
665
+ align-items: center;
666
+ gap: 8px;
667
+ }
668
+
669
+ .cell-indicator {
670
+ width: 6px;
671
+ height: 6px;
672
+ background: var(--trackio-chart-axis-text);
673
+ border: 1px solid var(--trackio-chart-axis-stroke);
674
+ opacity: 0.6;
675
+ flex-shrink: 0;
676
+ }
677
+
678
+ /* Oblivion theme: adjust cell styling to remove default border only in Oblivion */
679
+ :global(.trackio.theme--oblivion .cell) {
680
+ border: none !important;
681
+ background: transparent !important;
682
+ }
683
+
684
+ /* Classic theme: ensure borders are visible (redundant but explicit) */
685
+ :global(.trackio.theme--classic .cell) {
686
+ border: 1px solid var(--trackio-cell-border) !important;
687
+ background: var(--trackio-cell-background) !important;
688
+ border-radius: 10px !important;
689
+ }
690
+
691
+ :global(.trackio .cell-header) {
692
+ padding: 8px 10px;
693
+ display: flex;
694
+ align-items: center;
695
+ justify-content: space-between;
696
+ gap: 8px;
697
+ }
698
+
699
+ /* Oblivion theme: adjust header padding */
700
+ :global(.trackio.theme--oblivion .cell-header) {
701
+ padding: 5px 0px 18px 12px;
702
+ }
703
+
704
+ :global(.trackio .cell-title) {
705
+ font-size: 13px;
706
+ font-weight: 700;
707
+ color: var(--trackio-text-primary);
708
+ font-family: var(--trackio-font-family);
709
+ }
710
+
711
+ :global(.trackio .cell-body) {
712
+ position: relative;
713
+ width: 100%;
714
+ overflow: hidden;
715
+ }
716
+
717
+ :global(.trackio .cell-body svg) {
718
+ max-width: 100%;
719
+ height: auto;
720
+ display: block;
721
+ }
722
+
723
+ /* Theme: Oblivion overrides for cell layers - styles defined above */
724
+
725
+ /* Force Roboto Mono in Oblivion theme for cell elements */
726
+ :global(.trackio.theme--oblivion .cell-title) {
727
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
728
+ letter-spacing: 0.12em !important;
729
+ text-transform: uppercase !important;
730
+ font-weight: 800 !important;
731
+ font-size: 12px !important;
732
+ position: relative;
733
+ padding-left: 0;
734
+ }
735
+
736
+ /* Oblivion theme: add indicator dot before title */
737
+ :global(.trackio.theme--oblivion .cell-title)::before {
738
+ content: "";
739
+ position: absolute;
740
+ left: 0;
741
+ top: 50%;
742
+ transform: translateY(-50%);
743
+ width: 6px;
744
+ height: 6px;
745
+ background: var(--trackio-oblivion-primary);
746
+ border: 1px solid var(--trackio-oblivion-dim);
747
+ box-shadow: 0 0 10px color-mix(in srgb, var(--trackio-oblivion-base) 25%, transparent) inset;
748
+ opacity: 0.5;
749
+ }
750
+
751
+ /* Ghost hover effect */
752
+ :global(.trackio.hovering .ghost) {
753
+ opacity: 0.2;
754
+ transition: opacity 0.15s ease;
755
+ }
756
+
757
+ /* Wide cell spans full width */
758
+ :global(.trackio__grid .cell--wide) {
759
+ grid-column: 1 / -1;
760
+ }
761
+
762
+ /* Fullscreen button */
763
+ .cell-fullscreen-btn {
764
+ display: inline-flex;
765
+ align-items: center;
766
+ justify-content: center;
767
+ width: 32px;
768
+ height: 32px;
769
+ border: 0;
770
+ background: transparent;
771
+ color: var(--trackio-chart-axis-text);
772
+ opacity: 0.6;
773
+ cursor: pointer;
774
+ border-radius: 6px;
775
+ transition: opacity 0.15s ease;
776
+ }
777
+ .cell-fullscreen-btn:hover {
778
+ opacity: 1;
779
+ /* No background on hover - just opacity change */
780
+ }
781
+ .cell-fullscreen-btn svg {
782
+ width: 18px;
783
+ height: 18px;
784
+ fill: var(--trackio-chart-axis-text);
785
+ }
786
+
787
+ /* Fullscreen modal */
788
+ :global(.trackio-fullscreen-overlay) {
789
+ position: fixed;
790
+ inset: 0;
791
+ background: rgba(0, 0, 0, 0.85);
792
+ backdrop-filter: blur(8px);
793
+ display: flex;
794
+ align-items: center;
795
+ justify-content: center;
796
+ z-index: 9999;
797
+ opacity: 0;
798
+ pointer-events: none;
799
+ transition: opacity 0.3s ease;
800
+ }
801
+ :global(.trackio-fullscreen-overlay.is-open) {
802
+ opacity: 1;
803
+ pointer-events: auto;
804
+ }
805
+ :global(.trackio-fullscreen-modal) {
806
+ position: relative;
807
+ width: min(95vw, 1400px);
808
+ height: min(95vh, 900px);
809
+ background: var(--surface-bg);
810
+ border-radius: 12px;
811
+ overflow: hidden;
812
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
813
+ }
814
+ :global(.trackio-fullscreen-close) {
815
+ position: absolute;
816
+ top: 16px;
817
+ right: 16px;
818
+ width: 40px;
819
+ height: 40px;
820
+ border: 0;
821
+ background: rgba(0, 0, 0, 0.5);
822
+ color: white;
823
+ border-radius: 50%;
824
+ cursor: pointer;
825
+ display: flex;
826
+ align-items: center;
827
+ justify-content: center;
828
+ z-index: 10;
829
+ font-size: 20px;
830
+ transition: background-color 0.15s ease;
831
+ }
832
+ :global(.trackio-fullscreen-close:hover) {
833
+ background: rgba(0, 0, 0, 0.7);
834
+ }
835
+
836
+ /* Hide fullscreen button when in modal */
837
+ :global(.trackio-fullscreen-modal .cell-fullscreen-btn) {
838
+ display: none;
839
+ }
840
+
841
+ /* Minor ticks styling for logarithmic scales */
842
+ :global(.trackio .minor-ticks line.minor-tick) {
843
+ stroke: var(--trackio-chart-axis-stroke);
844
+ stroke-opacity: 0.4;
845
+ stroke-width: 0.5px;
846
+ }
847
+
848
+ /* Oblivion theme: enhanced minor ticks */
849
+ :global(.trackio.theme--oblivion .minor-ticks line.minor-tick) {
850
+ stroke: var(--trackio-oblivion-dim);
851
+ stroke-opacity: 0.6;
852
+ stroke-width: 0.8px;
853
+ }
854
+
855
+
856
+ /* Fullscreen cell takes full modal space */
857
+ :global(.trackio-fullscreen-modal .cell) {
858
+ width: 100%;
859
+ height: 100%;
860
+ border: none;
861
+ border-radius: 0;
862
+ }
863
+
864
+ :global(.trackio-fullscreen-modal .cell-inner) {
865
+ height: 100%;
866
+ }
867
+
868
+ :global(.trackio-fullscreen-modal .cell-body) {
869
+ flex: 1;
870
+ height: calc(100% - 50px); /* Minus header height */
871
+ }
872
+
873
+ :global(.trackio-fullscreen-modal .cell-body svg) {
874
+ width: 100% !important;
875
+ height: 100% !important;
876
+ max-width: none !important;
877
+ max-height: none !important;
878
+ }
879
+ </style>
880
+
881
+ <div class="cell {wide ? 'cell--wide' : ''}" bind:this={root} data-metric={metricKey} data-title={titleText} data-variant={variant}>
882
+ <div class="cell-bg"></div>
883
+ <div class="cell-corners"></div>
884
+ <div class="cell-inner">
885
+ <div class="cell-header">
886
+ <div class="cell-title">
887
+ {#if variant === 'oblivion'}
888
+ <span class="cell-indicator"></span>
889
+ {/if}
890
+ {titleText}
891
+ </div>
892
+ <button class="cell-fullscreen-btn" type="button" on:click={openFullscreen} title="Fullscreen">
893
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
894
+ <path d="M4 9V4h5v2H6v3H4zm10-5h5v5h-2V6h-3V4zM6 18h3v2H4v-5h2v3zm12-3h2v5h-5v-2h3v-3z"/>
895
+ </svg>
896
+ </button>
897
+ </div>
898
+ <div class="cell-body"><div bind:this={body}></div></div>
899
+ </div>
900
+ </div>
901
+
902
+
app/src/components/trackio/Legend.svelte ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { createEventDispatcher } from 'svelte';
3
+ export let items = [];
4
+ const dispatch = createEventDispatcher();
5
+ function enter(name){ dispatch('legend-hover', { name }); }
6
+ function leave(){ dispatch('legend-leave'); }
7
+ </script>
8
+
9
+ <div class="legend-bottom">
10
+ <div class="legend-title">Runs</div>
11
+ <div class="items">
12
+ {#each items as it}
13
+ <div class="item" role="presentation" data-run={it.name} on:mouseenter={() => enter(it.name)} on:mouseleave={leave}>
14
+ <span class="swatch" style={`background:${it.color}`}></span>
15
+ <span>{it.name}</span>
16
+ </div>
17
+ {/each}
18
+ </div>
19
+ </div>
20
+
21
+ <style>
22
+ /* =========================
23
+ LEGEND STYLES
24
+ ========================= */
25
+
26
+ .legend-bottom {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ gap: 6px;
31
+ font-size: 12px;
32
+ text-align: center;
33
+ width: 80%;
34
+ margin: 0 auto;
35
+ color: var(--trackio-legend-text);
36
+ font-family: var(--trackio-font-family);
37
+ }
38
+
39
+ /* Force Roboto Mono in Oblivion theme */
40
+ :global(.trackio.theme--oblivion) .legend-bottom,
41
+ :global(.trackio.theme--oblivion) .legend-title,
42
+ :global(.trackio.theme--oblivion) .item {
43
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
44
+ letter-spacing: 0.06em;
45
+ }
46
+
47
+ :global(.trackio.theme--oblivion) .legend-title {
48
+ font-weight: 900 !important;
49
+ letter-spacing: 0.18em !important;
50
+ text-transform: uppercase !important;
51
+ }
52
+
53
+ .legend-title {
54
+ font-size: 12px;
55
+ font-weight: 700;
56
+ color: var(--trackio-legend-text);
57
+ }
58
+
59
+ .items {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: 8px 14px;
63
+ justify-content: center;
64
+ align-items: center;
65
+ }
66
+
67
+ .item {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: 6px;
71
+ white-space: nowrap;
72
+ cursor: pointer;
73
+ padding: 2px 4px;
74
+ transition: opacity 0.15s ease;
75
+ color: var(--trackio-legend-text);
76
+ }
77
+
78
+ :global(.trackio.hovering) .item.ghost {
79
+ opacity: 0.2;
80
+ }
81
+
82
+ .swatch {
83
+ width: 14px;
84
+ height: 14px;
85
+ border-radius: 3px;
86
+ border: 1px solid var(--trackio-legend-swatch-border);
87
+ display: inline-block;
88
+ }
89
+ </style>
90
+
91
+
app/src/components/trackio/Tooltip.svelte ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ export let visible = false;
3
+ export let x = -9999;
4
+ export let y = -9999;
5
+ export let title = '';
6
+ export let subtitle = '';
7
+ export let entries = []; // [{ color, name, valueText }]
8
+ </script>
9
+
10
+ <div class="d3-tooltip {visible ? 'is-visible' : ''}" style={`transform: translate(${x}px, ${y}px);`}>
11
+ <div class="d3-tooltip__inner">
12
+ <div>{@html title}</div>
13
+ <div>{subtitle}</div>
14
+ {#each entries as e}
15
+ <div class="row">
16
+ <span class="dot" style={`background:${e.color}`}></span>
17
+ <strong>{e.name}</strong>
18
+ <span class="val">{e.valueText}</span>
19
+ </div>
20
+ {/each}
21
+ </div>
22
+
23
+ </div>
24
+
25
+ <style>
26
+ /* Tooltip variables for Oblivion theme */
27
+ :global(.trackio.theme--oblivion) .d3-tooltip {
28
+ /* Light mode variables */
29
+ --tooltip-oblivion-base: #2a2a2a;
30
+ --tooltip-oblivion-bg-primary: color-mix(in srgb, var(--tooltip-oblivion-base) 15%, transparent);
31
+ --tooltip-oblivion-bg-secondary: color-mix(in srgb, var(--tooltip-oblivion-base) 10%, transparent);
32
+ --tooltip-oblivion-bg-base: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
33
+ --tooltip-oblivion-border: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
34
+ --tooltip-oblivion-shadow: color-mix(in srgb, var(--tooltip-oblivion-base) 8%, transparent);
35
+ --tooltip-oblivion-text: color-mix(in srgb, var(--tooltip-oblivion-base) 90%, transparent);
36
+ --tooltip-oblivion-line-dark: rgba(0, 0, 0, 0.08);
37
+ --tooltip-oblivion-line-light: rgba(255, 255, 255, 0.15);
38
+ }
39
+
40
+ /* Dark mode variables */
41
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion) .d3-tooltip {
42
+ --tooltip-oblivion-base: #ffffff;
43
+ --tooltip-oblivion-bg-primary: color-mix(in srgb, var(--tooltip-oblivion-base) 15%, transparent);
44
+ --tooltip-oblivion-bg-secondary: color-mix(in srgb, var(--tooltip-oblivion-base) 10%, transparent);
45
+ --tooltip-oblivion-bg-base: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
46
+ --tooltip-oblivion-border: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
47
+ --tooltip-oblivion-shadow: color-mix(in srgb, var(--tooltip-oblivion-base)8%, transparent);
48
+ --tooltip-oblivion-text: color-mix(in srgb, var(--tooltip-oblivion-base) 90%, transparent);
49
+ --tooltip-oblivion-line-dark: rgba(0, 0, 0, 0.15);
50
+ --tooltip-oblivion-line-light: rgba(255, 255, 255, 0.08);
51
+ }
52
+
53
+ /* Classic tooltip styling */
54
+ .d3-tooltip {
55
+ position: absolute;
56
+ top: 0;
57
+ left: 0;
58
+ transform: translate(-9999px, -9999px);
59
+ pointer-events: none;
60
+ padding: 10px 12px;
61
+ border-radius: 12px;
62
+ font-size: 12px;
63
+ line-height: 1.35;
64
+ border: 1px solid var(--border-color);
65
+ background: var(--surface-bg);
66
+ color: var(--text-color);
67
+ box-shadow: 0 8px 32px rgba(0,0,0,.12), 0 2px 8px rgba(0,0,0,.06);
68
+ opacity: 0;
69
+ transition: opacity .12s ease;
70
+ z-index: var(--z-tooltip, 50);
71
+ backdrop-filter: saturate(1.12) blur(8px);
72
+ }
73
+ .d3-tooltip.is-visible { opacity: 1; }
74
+ .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; }
75
+ .d3-tooltip__inner > div:first-child { font-weight: 800; letter-spacing: 0.1px; margin-bottom: 0; }
76
+ .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--muted-color); display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.1px; }
77
+ .d3-tooltip__inner > div.row { padding-top: 6px; border-top: 1px solid var(--border-color); display:flex; align-items:center; gap:8px; white-space:nowrap; }
78
+ .dot { display:inline-block; width:12px; height:12px; border-radius:3px; border:1px solid var(--border-color); }
79
+ .val { margin-left:auto; text-align:right; }
80
+
81
+
82
+ /* Oblivion tooltip styling */
83
+ :global(.trackio.theme--oblivion) .d3-tooltip {
84
+ border-radius: 8px;
85
+ border: none;
86
+ background:
87
+ radial-gradient(400px 100px at 30% 0%, var(--tooltip-oblivion-bg-primary), transparent 70%),
88
+ radial-gradient(300px 80px at 70% 100%, var(--tooltip-oblivion-bg-secondary), transparent 70%),
89
+ var(--tooltip-oblivion-bg-base);
90
+ color: var(--tooltip-oblivion-text);
91
+ box-shadow:
92
+ 0 0 0 1px var(--tooltip-oblivion-border),
93
+ 0 8px 40px var(--tooltip-oblivion-shadow),
94
+ 0 2px 8px rgba(0, 0, 0, 0.05);
95
+ backdrop-filter: saturate(1.1) blur(10px);
96
+ opacity: 0.5;
97
+ }
98
+
99
+ :global(.trackio.theme--oblivion) .d3-tooltip.is-visible {
100
+ opacity: 1;
101
+ }
102
+
103
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div:first-child {
104
+ font-weight: 900;
105
+ letter-spacing: 0.18em;
106
+ text-transform: uppercase;
107
+ color: var(--tooltip-oblivion-text);
108
+ }
109
+
110
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div:nth-child(2) {
111
+ color: var(--tooltip-oblivion-text);
112
+ opacity: 0.4;
113
+ letter-spacing: 0.06em;
114
+ }
115
+
116
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div.row {
117
+ position: relative;
118
+ padding-top: 10px;
119
+ margin-top: 6px;
120
+ border-top: none;
121
+ }
122
+
123
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div.row::before {
124
+ content: "";
125
+ position: absolute;
126
+ left: 0;
127
+ right: 0;
128
+ top: 0;
129
+ height: 1px;
130
+ background: var(--tooltip-oblivion-line-dark);
131
+ }
132
+
133
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div.row::after {
134
+ content: "";
135
+ position: absolute;
136
+ left: 0;
137
+ right: 0;
138
+ top: 1px;
139
+ height: 1px;
140
+ background: var(--tooltip-oblivion-line-light);
141
+ }
142
+
143
+ :global(.trackio.theme--oblivion) .dot {
144
+ border-radius: 2px;
145
+ border: 1px solid var(--trackio-chart-axis-stroke);
146
+ box-shadow: 0 0 10px color-mix(in srgb, var(--trackio-oblivion-base) 20%, transparent) inset;
147
+ }
148
+ </style>
149
+
150
+
app/src/components/trackio/Trackio.svelte ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import * as d3 from 'd3';
3
+ import { formatAbbrev, smoothMetricData } from './chart-utils.js';
4
+ import { generateRunNames, genCurves } from './data-generator.js';
5
+ import Legend from './Legend.svelte';
6
+ import Cell from './Cell.svelte';
7
+ import { onMount, onDestroy } from 'svelte';
8
+ import { jitterTrigger } from './store.js';
9
+
10
+ export let variant = 'classic'; // 'classic' | 'oblivion'
11
+ export let normalizeLoss = true;
12
+ export let logScaleX = false;
13
+ export let smoothing = false;
14
+
15
+ let hostEl;
16
+ let gridEl;
17
+ let legendItems = [];
18
+ const cellsDef = [
19
+ { metric:'epoch', title:'Epoch' },
20
+ { metric:'train_accuracy', title:'Train accuracy' },
21
+ { metric:'train_loss', title:'Train loss' },
22
+ { metric:'val_accuracy', title:'Val accuracy' },
23
+ { metric:'val_loss', title:'Val loss', wide:true }
24
+ ];
25
+ let preparedData = {};
26
+ let colorsByRun = {};
27
+
28
+ // Variables for data management (will be initialized in onMount)
29
+ let dataByMetric = new Map();
30
+ let metricsToDraw = [];
31
+ let currentRunList = [];
32
+ let cycleIdx = 2;
33
+
34
+ // Dynamic color palette using color-palettes.js helper
35
+ let dynamicPalette = ['#0ea5e9', '#8b5cf6', '#f59e0b', '#ef4444', '#10b981', '#f97316', '#3b82f6', '#8b5ad6']; // fallback
36
+
37
+ const updateDynamicPalette = () => {
38
+ if (typeof window !== 'undefined' && window.ColorPalettes && currentRunList.length > 0) {
39
+ try {
40
+ dynamicPalette = window.ColorPalettes.getColors('categorical', currentRunList.length);
41
+ } catch (e) {
42
+ console.warn('Failed to generate dynamic palette:', e);
43
+ // Keep fallback palette
44
+ }
45
+ }
46
+ };
47
+
48
+ const colorForRun = (name) => {
49
+ const idx = currentRunList.indexOf(name);
50
+ return idx >= 0 ? dynamicPalette[idx % dynamicPalette.length] : '#999';
51
+ };
52
+
53
+
54
+ // Jitter function - generates completely new data with new runs
55
+ function jitterData(){
56
+ console.log('jitterData called - generating new data with random number of runs'); // Debug log
57
+
58
+ // Generate new random data with weighted probability for fewer runs
59
+ // Higher probability for 2-3 runs, lower for 4-5-6 runs
60
+ const rand = Math.random();
61
+ let wantRuns;
62
+ if (rand < 0.4) wantRuns = 2; // 40% chance
63
+ else if (rand < 0.7) wantRuns = 3; // 30% chance
64
+ else if (rand < 0.85) wantRuns = 4; // 15% chance
65
+ else if (rand < 0.95) wantRuns = 5; // 10% chance
66
+ else wantRuns = 6; // 5% chance
67
+ const runsSim = generateRunNames(wantRuns);
68
+ const rnd = (min,max)=> Math.floor(min + Math.random()*(max-min+1));
69
+ let stepsCount = rnd(80, 240); // Random number of steps
70
+ const steps = Array.from({length: stepsCount}, (_,i)=> i+1);
71
+ const nextByMetric = new Map();
72
+ const TARGET_METRICS = ['epoch', 'train_accuracy', 'train_loss', 'val_accuracy', 'val_loss'];
73
+
74
+ // Initialize data structure
75
+ TARGET_METRICS.forEach((tgt) => {
76
+ const map = {};
77
+ runsSim.forEach((r) => { map[r] = []; });
78
+ nextByMetric.set(tgt, map);
79
+ });
80
+
81
+ // Generate curves for each run
82
+ runsSim.forEach(run => {
83
+ const curves = genCurves(stepsCount);
84
+ steps.forEach((s,i)=>{
85
+ nextByMetric.get('epoch')[run].push({ step:s, value:s });
86
+ nextByMetric.get('train_accuracy')[run].push({ step:s, value: curves.accTrain[i] });
87
+ nextByMetric.get('val_accuracy')[run].push({ step:s, value: curves.accVal[i] });
88
+ nextByMetric.get('train_loss')[run].push({ step:s, value: curves.lossTrain[i] });
89
+ nextByMetric.get('val_loss')[run].push({ step:s, value: curves.lossVal[i] });
90
+ });
91
+ });
92
+
93
+ // Update all reactive data
94
+ nextByMetric.forEach((v,k)=> dataByMetric.set(k,v));
95
+ metricsToDraw = TARGET_METRICS;
96
+ currentRunList = runsSim.slice();
97
+ updateDynamicPalette(); // Generate new colors based on run count
98
+ legendItems = currentRunList.map((name) => ({ name, color: colorForRun(name) }));
99
+ updatePreparedData();
100
+ colorsByRun = Object.fromEntries(currentRunList.map((name) => [name, colorForRun(name)]));
101
+
102
+ console.log(`jitterData completed - generated ${wantRuns} runs with ${stepsCount} steps`); // Debug log
103
+ }
104
+
105
+ // Public API: allow external theme switch
106
+ function setTheme(name){
107
+ variant = name === 'oblivion' ? 'oblivion' : 'classic';
108
+ updateThemeClass();
109
+
110
+ // Debug log for font application
111
+ if (typeof window !== 'undefined') {
112
+ console.log(`Theme switched to: ${variant}`);
113
+ if (hostEl) {
114
+ const computedStyle = getComputedStyle(hostEl);
115
+ const appliedFont = computedStyle.fontFamily;
116
+ console.log(`Applied font-family: ${appliedFont}`);
117
+ }
118
+ }
119
+ }
120
+
121
+ // Public API: allow external log scale X toggle
122
+ function setLogScaleX(enabled) {
123
+ logScaleX = enabled;
124
+ console.log(`Log scale X set to: ${logScaleX}`);
125
+ }
126
+
127
+ // Public API: allow external smoothing toggle
128
+ function setSmoothing(enabled) {
129
+ smoothing = enabled;
130
+ console.log(`Smoothing set to: ${smoothing}`);
131
+ // Re-prepare data with smoothing applied
132
+ updatePreparedData();
133
+ }
134
+
135
+ // Update prepared data with optional smoothing
136
+ let preparedRawData = {}; // Store original data for background display
137
+
138
+ function updatePreparedData() {
139
+ const TARGET_METRICS = ['epoch', 'train_accuracy', 'train_loss', 'val_accuracy', 'val_loss'];
140
+ let dataToUse = {};
141
+ let rawDataToStore = {};
142
+
143
+ TARGET_METRICS.forEach(metric => {
144
+ const rawData = dataByMetric.get(metric);
145
+ if (rawData) {
146
+ // Store original data
147
+ rawDataToStore[metric] = rawData;
148
+
149
+ // Apply smoothing if enabled (except for epoch which should stay exact)
150
+ dataToUse[metric] = (smoothing && metric !== 'epoch')
151
+ ? smoothMetricData(rawData, 5) // Window size of 5
152
+ : rawData;
153
+ }
154
+ });
155
+
156
+ preparedData = dataToUse;
157
+ preparedRawData = rawDataToStore;
158
+ console.log(`Prepared data updated, smoothing: ${smoothing}`);
159
+ }
160
+
161
+ function updateThemeClass(){
162
+ if (!hostEl) return;
163
+ hostEl.classList.toggle('theme--classic', variant === 'classic');
164
+ hostEl.classList.toggle('theme--oblivion', variant === 'oblivion');
165
+ hostEl.setAttribute('data-variant', variant);
166
+ }
167
+
168
+ $: updateThemeClass();
169
+
170
+
171
+ // Chart logic now handled by Cell.svelte
172
+
173
+ let cleanup = null;
174
+ onMount(() => {
175
+ if (!hostEl || !gridEl) return;
176
+ hostEl.__setTheme = setTheme;
177
+
178
+ // Jitter & Simulate functions
179
+ function rebuildLegend(){
180
+ updateDynamicPalette(); // Update colors when adding new data
181
+ legendItems = currentRunList.map((name) => ({ name, color: colorForRun(name) }));
182
+ }
183
+
184
+ function simulateData(){
185
+ // Generate new random data with weighted probability for fewer runs
186
+ // Higher probability for 2-3 runs, lower for 4-5-6 runs
187
+ const rand = Math.random();
188
+ let wantRuns;
189
+ if (rand < 0.4) wantRuns = 2; // 40% chance
190
+ else if (rand < 0.7) wantRuns = 3; // 30% chance
191
+ else if (rand < 0.85) wantRuns = 4; // 15% chance
192
+ else if (rand < 0.95) wantRuns = 5; // 10% chance
193
+ else wantRuns = 6; // 5% chance
194
+ const runsSim = generateRunNames(wantRuns);
195
+ const rnd = (min,max)=> Math.floor(min + Math.random()*(max-min+1));
196
+ let stepsCount = 16;
197
+ if (cycleIdx === 0) stepsCount = rnd(4, 12); else if (cycleIdx === 1) stepsCount = rnd(16, 48); else stepsCount = rnd(80, 240);
198
+ cycleIdx = (cycleIdx + 1) % 3;
199
+ const steps = Array.from({length: stepsCount}, (_,i)=> i+1);
200
+ const nextByMetric = new Map();
201
+ const TARGET_METRICS = ['epoch', 'train_accuracy', 'train_loss', 'val_accuracy', 'val_loss'];
202
+ const mList = (metricsToDraw && metricsToDraw.length) ? metricsToDraw : TARGET_METRICS;
203
+ mList.forEach((tgt) => {
204
+ const map = {};
205
+ runsSim.forEach((r) => { map[r] = []; });
206
+ nextByMetric.set(tgt, map);
207
+ });
208
+ runsSim.forEach(run => {
209
+ const curves = genCurves(stepsCount);
210
+ steps.forEach((s,i)=>{
211
+ if (mList.includes('epoch')) nextByMetric.get('epoch')[run].push({ step:s, value:s });
212
+ if (mList.includes('train_accuracy')) nextByMetric.get('train_accuracy')[run].push({ step:s, value: curves.accTrain[i] });
213
+ if (mList.includes('val_accuracy')) nextByMetric.get('val_accuracy')[run].push({ step:s, value: curves.accVal[i] });
214
+ if (mList.includes('train_loss')) nextByMetric.get('train_loss')[run].push({ step:s, value: curves.lossTrain[i] });
215
+ if (mList.includes('val_loss')) nextByMetric.get('val_loss')[run].push({ step:s, value: curves.lossVal[i] });
216
+ });
217
+ });
218
+ nextByMetric.forEach((v,k)=> dataByMetric.set(k,v));
219
+ currentRunList = runsSim.slice();
220
+ rebuildLegend();
221
+ updatePreparedData();
222
+ updateDynamicPalette(); // Update colors when rebuilding
223
+ colorsByRun = Object.fromEntries(currentRunList.map((name) => [name, colorForRun(name)]));
224
+ }
225
+ // No need for event listeners anymore - we'll use reactive statement
226
+
227
+ // Start with level 3 long synthetic data for consistency
228
+ simulateData();
229
+ // Svelte Cells will react to preparedData/colorsByRun updates
230
+
231
+ cleanup = () => {
232
+ // No cleanup needed for reactive statements
233
+ };
234
+ });
235
+
236
+ onDestroy(() => { if (cleanup) cleanup(); });
237
+
238
+ // Expose instance for debugging and external theme control
239
+ onMount(() => {
240
+ window.trackioInstance = { jitterData };
241
+ if (hostEl) {
242
+ hostEl.__trackioInstance = { setTheme, setLogScaleX, setSmoothing, jitterData };
243
+ }
244
+
245
+ // Initialize dynamic palette
246
+ updateDynamicPalette();
247
+
248
+ // Listen for palette updates from color-palettes.js
249
+ const handlePaletteUpdate = () => {
250
+ updateDynamicPalette();
251
+ // Rebuild legend and colors if needed
252
+ if (currentRunList.length > 0) {
253
+ legendItems = currentRunList.map((name) => ({ name, color: colorForRun(name) }));
254
+ colorsByRun = Object.fromEntries(currentRunList.map((name) => [name, colorForRun(name)]));
255
+ }
256
+ };
257
+
258
+ document.addEventListener('palettes:updated', handlePaletteUpdate);
259
+
260
+ // Cleanup listener on destroy
261
+ return () => {
262
+ document.removeEventListener('palettes:updated', handlePaletteUpdate);
263
+ };
264
+ });
265
+
266
+ // React to jitter trigger from store
267
+ $: {
268
+ console.log('Reactive statement triggered, jitterTrigger value:', $jitterTrigger);
269
+ if ($jitterTrigger > 0) {
270
+ console.log('Jitter trigger activated:', $jitterTrigger, 'calling jitterData()');
271
+ jitterData();
272
+ }
273
+ }
274
+
275
+ // Legend ghost helpers (hover effects)
276
+ function ghostRun(run){
277
+ try {
278
+ hostEl.classList.add('hovering');
279
+
280
+ // Ghost the chart lines and points
281
+ hostEl.querySelectorAll('.cell').forEach(cell => {
282
+ cell.querySelectorAll('svg .lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
283
+ cell.querySelectorAll('svg .points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
284
+ });
285
+
286
+ // Ghost the legend items
287
+ hostEl.querySelectorAll('.legend-bottom .item').forEach(item => {
288
+ const itemRun = item.getAttribute('data-run');
289
+ item.classList.toggle('ghost', itemRun !== run);
290
+ });
291
+ } catch(_) {}
292
+ }
293
+ function clearGhost(){
294
+ try {
295
+ hostEl.classList.remove('hovering');
296
+
297
+ // Clear ghost from chart lines and points
298
+ hostEl.querySelectorAll('.cell').forEach(cell => {
299
+ cell.querySelectorAll('svg .lines path.run-line').forEach(p => p.classList.remove('ghost'));
300
+ cell.querySelectorAll('svg .points circle.pt').forEach(c => c.classList.remove('ghost'));
301
+ });
302
+
303
+ // Clear ghost from legend items
304
+ hostEl.querySelectorAll('.legend-bottom .item').forEach(item => {
305
+ item.classList.remove('ghost');
306
+ });
307
+ } catch(_) {}
308
+ }
309
+ </script>
310
+
311
+ <div class="trackio theme--classic" bind:this={hostEl} data-variant={variant}>
312
+ <div class="trackio__header">
313
+ <Legend items={legendItems} on:legend-hover={(e) => { const run = e?.detail?.name; if (!run) return; ghostRun(run); }} on:legend-leave={() => { clearGhost(); }} />
314
+ </div>
315
+ <div class="trackio__grid" bind:this={gridEl}>
316
+ {#each cellsDef as c}
317
+ <Cell metricKey={c.metric} titleText={c.title} wide={c.wide} variant={variant} normalizeLoss={normalizeLoss} logScaleX={logScaleX} smoothing={smoothing} metricData={(preparedData && preparedData[c.metric]) || {}} rawMetricData={(preparedRawData && preparedRawData[c.metric]) || {}} colorForRun={(name)=> colorsByRun[name] || '#999'} hostEl={hostEl} />
318
+ {/each}
319
+ </div>
320
+ </div>
321
+
322
+ <style>
323
+ /* =========================
324
+ TRACKIO THEME SYSTEM
325
+ ========================= */
326
+
327
+ /* Font imports for themes - ensure Roboto Mono is loaded for Oblivion theme */
328
+ @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap');
329
+
330
+ /* Fallback font-face declaration */
331
+ @font-face {
332
+ font-family: 'Roboto Mono Fallback';
333
+ src: url('https://fonts.gstatic.com/s/robotomono/v23/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW4AJi8SJQt.woff2') format('woff2');
334
+ font-weight: 400;
335
+ font-style: normal;
336
+ font-display: swap;
337
+ }
338
+
339
+ /* Base variables - all themes inherit these */
340
+ .trackio {
341
+ position: relative;
342
+ --z-tooltip: 50;
343
+ --z-overlay: 99999999;
344
+
345
+ /* Typography */
346
+ --trackio-font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
347
+ --trackio-font-weight-normal: 400;
348
+ --trackio-font-weight-medium: 600;
349
+ --trackio-font-weight-bold: 700;
350
+
351
+ /* Apply font-family to root element */
352
+ font-family: var(--trackio-font-family);
353
+
354
+ /* Base color system for Classic theme */
355
+ --trackio-base: #323232;
356
+ --trackio-primary: var(--trackio-base);
357
+ --trackio-dim: color-mix(in srgb, var(--trackio-base) 28%, transparent);
358
+ --trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
359
+ --trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
360
+
361
+ /* Chart rendering */
362
+ --trackio-chart-grid-type: 'lines'; /* 'lines' | 'dots' */
363
+ --trackio-chart-axis-stroke: var(--trackio-dim);
364
+ --trackio-chart-axis-text: var(--trackio-text);
365
+ --trackio-chart-grid-stroke: var(--trackio-subtle);
366
+ --trackio-chart-grid-opacity: 1;
367
+ }
368
+
369
+ /* Dark mode overrides for Classic theme */
370
+ :global([data-theme="dark"]) .trackio.theme--classic {
371
+ --trackio-base: #ffffff;
372
+ --trackio-primary: var(--trackio-base);
373
+ --trackio-dim: color-mix(in srgb, var(--trackio-base) 25%, transparent);
374
+ --trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
375
+ --trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
376
+
377
+ /* Cell background for dark mode */
378
+ --trackio-cell-background: rgba(255, 255, 255, 0.03);
379
+ }
380
+
381
+ .trackio.theme--classic {
382
+ /* Cell styling */
383
+ --trackio-cell-background: rgba(0, 0, 0, 0.02);
384
+ --trackio-cell-border: var(--border-color, rgba(0, 0, 0, 0.1));
385
+ --trackio-cell-corner-inset: 0px;
386
+ --trackio-cell-gap: 12px;
387
+
388
+ /* Typography */
389
+ --trackio-text-primary: var(--text-color, rgba(0, 0, 0, 0.9));
390
+ --trackio-text-secondary: var(--muted-color, rgba(0, 0, 0, 0.6));
391
+ --trackio-text-accent: var(--primary-color, #E889AB);
392
+
393
+ /* Tooltip */
394
+ --trackio-tooltip-background: var(--surface-bg, white);
395
+ --trackio-tooltip-border: var(--border-color, rgba(0, 0, 0, 0.1));
396
+ --trackio-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
397
+
398
+ /* Legend */
399
+ --trackio-legend-text: var(--text-color, rgba(0, 0, 0, 0.9));
400
+ --trackio-legend-swatch-border: var(--border-color, rgba(0, 0, 0, 0.1));
401
+ }
402
+
403
+ /* Dark mode adjustments */
404
+ :global([data-theme="dark"]) .trackio {
405
+ --trackio-chart-axis-stroke: rgba(255, 255, 255, 0.18);
406
+ --trackio-chart-axis-text: rgba(255, 255, 255, 0.60);
407
+ --trackio-chart-grid-stroke: rgba(255, 255, 255, 0.08);
408
+ }
409
+
410
+ /* =========================
411
+ THEME: CLASSIC (Default)
412
+ ========================= */
413
+
414
+ .trackio.theme--classic {
415
+ /* Keep default values - no overrides needed */
416
+ }
417
+
418
+ /* =========================
419
+ THEME: OBLIVION
420
+ ========================= */
421
+
422
+ .trackio.theme--oblivion {
423
+ /* Core oblivion color system - Light mode: darker colors for visibility */
424
+ --trackio-oblivion-base: #2a2a2a;
425
+ --trackio-oblivion-primary: var(--trackio-oblivion-base);
426
+ --trackio-oblivion-dim: color-mix(in srgb, var(--trackio-oblivion-base) 30%, transparent);
427
+ --trackio-oblivion-subtle: color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent);
428
+ --trackio-oblivion-ghost: color-mix(in srgb, var(--trackio-oblivion-base) 4%, transparent);
429
+
430
+ /* Chart rendering overrides */
431
+ --trackio-chart-grid-type: 'dots';
432
+ --trackio-chart-axis-stroke: var(--trackio-oblivion-dim);
433
+ --trackio-chart-axis-text: var(--trackio-oblivion-primary);
434
+ --trackio-chart-grid-stroke: var(--trackio-oblivion-dim);
435
+ --trackio-chart-grid-opacity: 0.6;
436
+ }
437
+
438
+ /* Dark mode overrides for Oblivion theme */
439
+ :global([data-theme="dark"]) .trackio.theme--oblivion {
440
+ --trackio-oblivion-base: #ffffff;
441
+ --trackio-oblivion-primary: var(--trackio-oblivion-base);
442
+ --trackio-oblivion-dim: color-mix(in srgb, var(--trackio-oblivion-base) 25%, transparent);
443
+ --trackio-oblivion-subtle: color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent);
444
+ --trackio-oblivion-ghost: color-mix(in srgb, var(--trackio-oblivion-base) 4%, transparent);
445
+ }
446
+
447
+ .trackio.theme--oblivion {
448
+ /* Cell styling overrides */
449
+ --trackio-cell-background: var(--trackio-oblivion-subtle);
450
+ --trackio-cell-border: var(--trackio-oblivion-dim);
451
+ --trackio-cell-corner-inset: 6px;
452
+ --trackio-cell-gap: 0px;
453
+
454
+ /* HUD-specific variables */
455
+ --trackio-oblivion-hud-gap: 10px;
456
+ --trackio-oblivion-hud-corner-size: 8px;
457
+ --trackio-oblivion-hud-bg-gradient:
458
+ radial-gradient(1200px 200px at 20% -10%, var(--trackio-oblivion-ghost), transparent 80%),
459
+ radial-gradient(900px 200px at 80% 110%, var(--trackio-oblivion-ghost), transparent 80%);
460
+
461
+ /* Typography overrides */
462
+ --trackio-text-primary: var(--trackio-oblivion-primary);
463
+ --trackio-text-secondary: var(--trackio-oblivion-dim);
464
+ --trackio-text-accent: var(--trackio-oblivion-primary);
465
+
466
+ /* Tooltip overrides */
467
+ --trackio-tooltip-background: var(--trackio-oblivion-subtle);
468
+ --trackio-tooltip-border: var(--trackio-oblivion-dim);
469
+ --trackio-tooltip-shadow:
470
+ 0 8px 32px color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent),
471
+ 0 2px 8px color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent);
472
+
473
+ /* Legend overrides */
474
+ --trackio-legend-text: var(--trackio-oblivion-primary);
475
+ --trackio-legend-swatch-border: var(--trackio-oblivion-dim);
476
+
477
+ /* Font styling overrides */
478
+ --trackio-font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace;
479
+ font-family: var(--trackio-font-family) !important;
480
+ color: var(--trackio-text-primary);
481
+ }
482
+
483
+ /* Force Roboto Mono application in Oblivion theme */
484
+ .trackio.theme--oblivion,
485
+ .trackio.theme--oblivion * {
486
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
487
+ }
488
+
489
+ /* Specific overrides for different elements in Oblivion */
490
+ .trackio.theme--oblivion .cell-title,
491
+ .trackio.theme--oblivion .legend-bottom,
492
+ .trackio.theme--oblivion .legend-title,
493
+ .trackio.theme--oblivion .item {
494
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
495
+ }
496
+
497
+ /* Dark mode adjustments for Oblivion */
498
+ :global([data-theme="dark"]) .trackio.theme--oblivion {
499
+ --trackio-oblivion-base: #ffffff;
500
+ --trackio-oblivion-hud-bg-gradient:
501
+ radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, var(--trackio-oblivion-base) 6.5%, transparent), transparent 80%),
502
+ radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent), transparent 80%),
503
+ linear-gradient(180deg, color-mix(in srgb, var(--trackio-oblivion-base) 3.5%, transparent), transparent 45%);
504
+
505
+ --trackio-tooltip-shadow:
506
+ 0 8px 32px color-mix(in srgb, var(--trackio-oblivion-base) 5%, transparent),
507
+ 0 2px 8px color-mix(in srgb, black 10%, transparent);
508
+
509
+ background: #0f1115;
510
+ }
511
+
512
+ /* =========================
513
+ LAYOUT & COMPONENTS
514
+ ========================= */
515
+
516
+ .trackio__grid {
517
+ display: grid;
518
+ grid-template-columns: repeat(2, minmax(0, 1fr));
519
+ gap: var(--trackio-cell-gap);
520
+ }
521
+
522
+ @media (max-width: 980px) {
523
+ .trackio__grid { grid-template-columns: 1fr; }
524
+ }
525
+
526
+ .trackio__header {
527
+ display: flex;
528
+ align-items: flex-start;
529
+ justify-content: center;
530
+ gap: 12px;
531
+ margin: 0 0 10px 0;
532
+ flex-wrap: wrap;
533
+ width: 100%;
534
+ }
535
+
536
+ /* Legacy axis/grid selectors - for compatibility with Cell.svelte */
537
+ .trackio .axes path,
538
+ .trackio .axes line {
539
+ stroke: var(--trackio-chart-axis-stroke);
540
+ }
541
+
542
+ .trackio .axes text {
543
+ fill: var(--trackio-chart-axis-text);
544
+ font-family: var(--trackio-font-family);
545
+ }
546
+
547
+ /* Force font-family for SVG text in Oblivion */
548
+ .trackio.theme--oblivion .axes text {
549
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
550
+ }
551
+
552
+ .trackio .grid line {
553
+ stroke: var(--trackio-chart-grid-stroke);
554
+ opacity: var(--trackio-chart-grid-opacity);
555
+ }
556
+
557
+ /* Grid type switching */
558
+ .trackio .grid-dots { display: none; }
559
+ .trackio.theme--oblivion .grid { display: none; }
560
+ .trackio.theme--oblivion .grid-dots { display: block; }
561
+ .trackio.theme--oblivion .cell-bg,
562
+ .trackio.theme--oblivion .cell-corners { display: block; }
563
+ </style>
564
+
565
+
app/src/components/trackio/chart-utils.js ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Chart utilities for axis formatting and tick generation
2
+
3
+ export const formatAbbrev = (value) => {
4
+ const num = Number(value);
5
+ if (!Number.isFinite(num)) return String(value);
6
+ const abs = Math.abs(num);
7
+ const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, '');
8
+ if (abs >= 1e9) return `${trim2(num / 1e9)}B`;
9
+ if (abs >= 1e6) return `${trim2(num / 1e6)}M`;
10
+ if (abs >= 1e3) return `${trim2(num / 1e3)}K`;
11
+ return trim2(num);
12
+ };
13
+
14
+ /**
15
+ * Enhanced formatting for logarithmic scale ticks
16
+ * @param {number} value - The tick value
17
+ * @param {boolean} isLogScale - Whether this is for a log scale
18
+ * @returns {string} Formatted tick label
19
+ */
20
+ export const formatLogTick = (value, isLogScale = false) => {
21
+ if (!isLogScale) return formatAbbrev(value);
22
+
23
+ const num = Number(value);
24
+ if (!Number.isFinite(num)) return String(value);
25
+
26
+ // Check if it's a power of 10
27
+ const log10 = Math.log10(Math.abs(num));
28
+ const isPowerOf10 = Math.abs(log10 % 1) < 0.01;
29
+
30
+ if (isPowerOf10) {
31
+ // Format powers of 10 more prominently
32
+ const power = Math.round(log10);
33
+ if (power >= 0 && power <= 6) {
34
+ // For small powers, show the actual number
35
+ return formatAbbrev(value);
36
+ } else {
37
+ // For very large/small powers, use scientific notation
38
+ return `10^${power}`;
39
+ }
40
+ }
41
+
42
+ // For non-powers of 10, use regular formatting
43
+ return formatAbbrev(value);
44
+ };
45
+
46
+ /**
47
+ * Generates optimized tick positions for logarithmic scales
48
+ * @param {Array} stepValues - Array of actual step values (not indices)
49
+ * @param {number} minTicks - Minimum number of ticks desired
50
+ * @param {number} maxTicks - Maximum number of ticks allowed
51
+ * @param {number} width - Chart width in pixels
52
+ * @param {Function} scale - D3 log scale function
53
+ * @returns {Object} Object with major and minor tick positions
54
+ */
55
+ export function generateLogTicks(stepValues, minTicks, maxTicks, width, scale) {
56
+ if (!stepValues || stepValues.length === 0 || !scale) return { major: [], minor: [] };
57
+
58
+ const minPixelSpacing = 50; // Reduced for better density
59
+ const minorPixelSpacing = 25; // Spacing for minor ticks
60
+ const maxTicksFromWidth = Math.max(4, Math.floor(width / minPixelSpacing));
61
+ const targetMaxTicks = Math.min(maxTicks, maxTicksFromWidth);
62
+
63
+ // Debug logging
64
+ console.log('🎯 generateLogTicks called:', {
65
+ stepCount: stepValues.length,
66
+ stepRange: [Math.min(...stepValues), Math.max(...stepValues)],
67
+ targetTicks: [minTicks, targetMaxTicks],
68
+ width
69
+ });
70
+
71
+ const domain = scale.domain();
72
+ const [minVal, maxVal] = domain;
73
+
74
+ // Calculate the range in log space
75
+ const logMin = Math.log10(minVal);
76
+ const logMax = Math.log10(maxVal);
77
+ const logRange = logMax - logMin;
78
+
79
+ // Generate major ticks (powers of 10)
80
+ const majorCandidates = new Set();
81
+ const minorCandidates = new Set();
82
+
83
+ // Always add domain boundaries
84
+ majorCandidates.add(minVal);
85
+ majorCandidates.add(maxVal);
86
+
87
+ const startPower = Math.floor(logMin);
88
+ const endPower = Math.ceil(logMax);
89
+
90
+ // Major ticks: powers of 10
91
+ for (let power = startPower; power <= endPower; power++) {
92
+ const value = Math.pow(10, power);
93
+ if (value >= minVal && value <= maxVal) {
94
+ majorCandidates.add(value);
95
+ }
96
+ }
97
+
98
+ // If we have space, add more major ticks (2x, 5x)
99
+ if (logRange > 0.7) {
100
+ for (let power = startPower; power <= endPower; power++) {
101
+ const base = Math.pow(10, power);
102
+ [2, 5].forEach(multiplier => {
103
+ const value = base * multiplier;
104
+ if (value >= minVal && value <= maxVal) {
105
+ majorCandidates.add(value);
106
+ }
107
+ });
108
+ }
109
+ }
110
+
111
+ // Minor ticks: intermediate values to show log progression
112
+ for (let power = startPower; power <= endPower; power++) {
113
+ const base = Math.pow(10, power);
114
+ // Add 3x, 4x, 6x, 7x, 8x, 9x for visual density
115
+ [3, 4, 6, 7, 8, 9].forEach(multiplier => {
116
+ const value = base * multiplier;
117
+ if (value >= minVal && value <= maxVal && !majorCandidates.has(value)) {
118
+ minorCandidates.add(value);
119
+ }
120
+ });
121
+ }
122
+
123
+ // Match candidates to actual step values
124
+ const matchToStepValues = (candidates) => {
125
+ return Array.from(candidates).map(candidate => {
126
+ let closest = stepValues[0];
127
+ let minRelativeDistance = Math.abs(stepValues[0] - candidate) / Math.max(stepValues[0], candidate);
128
+
129
+ stepValues.forEach(step => {
130
+ const relativeDistance = Math.abs(step - candidate) / Math.max(step, candidate);
131
+ if (relativeDistance < minRelativeDistance) {
132
+ minRelativeDistance = relativeDistance;
133
+ closest = step;
134
+ }
135
+ });
136
+
137
+ // Only include if reasonable match (20% tolerance for minor, 15% for major)
138
+ const isPowerOf10 = Math.abs(Math.log10(candidate) % 1) < 0.01;
139
+ const tolerance = majorCandidates.has(candidate) ? 0.15 : 0.20;
140
+
141
+ if (minRelativeDistance < tolerance || isPowerOf10) {
142
+ return closest;
143
+ }
144
+ return null;
145
+ }).filter(v => v !== null);
146
+ };
147
+
148
+ let majorTicks = Array.from(new Set(matchToStepValues(majorCandidates))).sort((a, b) => a - b);
149
+ let minorTicks = Array.from(new Set(matchToStepValues(minorCandidates))).sort((a, b) => a - b);
150
+
151
+ // Filter major ticks by pixel spacing
152
+ const filteredMajorTicks = [];
153
+ majorTicks.forEach(tick => {
154
+ if (filteredMajorTicks.length === 0) {
155
+ filteredMajorTicks.push(tick);
156
+ } else {
157
+ const prevTick = filteredMajorTicks[filteredMajorTicks.length - 1];
158
+ const pixelDistance = Math.abs(scale(tick) - scale(prevTick));
159
+ if (pixelDistance >= minPixelSpacing) {
160
+ filteredMajorTicks.push(tick);
161
+ }
162
+ }
163
+ });
164
+
165
+ // Filter minor ticks by pixel spacing and ensure they don't conflict with major ticks
166
+ const filteredMinorTicks = [];
167
+ minorTicks.forEach(tick => {
168
+ // Skip if too close to any major tick
169
+ const tooCloseToMajor = filteredMajorTicks.some(majorTick => {
170
+ const distance = Math.abs(scale(tick) - scale(majorTick));
171
+ return distance < minorPixelSpacing;
172
+ });
173
+
174
+ if (!tooCloseToMajor) {
175
+ // Check spacing with previous minor tick
176
+ if (filteredMinorTicks.length === 0) {
177
+ filteredMinorTicks.push(tick);
178
+ } else {
179
+ const prevTick = filteredMinorTicks[filteredMinorTicks.length - 1];
180
+ const pixelDistance = Math.abs(scale(tick) - scale(prevTick));
181
+ if (pixelDistance >= minorPixelSpacing) {
182
+ filteredMinorTicks.push(tick);
183
+ }
184
+ }
185
+ }
186
+ });
187
+
188
+ const result = {
189
+ major: filteredMajorTicks.length >= 2 ? filteredMajorTicks : [minVal, maxVal],
190
+ minor: filteredMinorTicks
191
+ };
192
+
193
+ // Debug logging
194
+ console.log('🎯 generateLogTicks result:', {
195
+ logRange: logRange.toFixed(2),
196
+ majorCount: result.major.length,
197
+ minorCount: result.minor.length,
198
+ majorTicks: result.major,
199
+ minorTicks: result.minor
200
+ });
201
+
202
+ return result;
203
+ }
204
+
205
+ /**
206
+ * Generates intelligent tick positions for X-axis with nice intervals
207
+ * @param {Array} steps - Array of step values (e.g., [1, 2, 3, ..., 100])
208
+ * @param {number} minTicks - Minimum number of ticks desired
209
+ * @param {number} maxTicks - Maximum number of ticks allowed
210
+ * @param {number} width - Chart width in pixels
211
+ * @returns {Array} Array of step indices for tick positions
212
+ */
213
+ export function generateSmartTicks(steps, minTicks, maxTicks, width) {
214
+ if (!steps || steps.length === 0) return [];
215
+
216
+ const totalSteps = steps.length;
217
+ const minPixelSpacing = 75; // Slightly reduced minimum spacing to allow more ticks
218
+ const maxTicksFromWidth = Math.max(3, Math.floor(width / minPixelSpacing));
219
+
220
+ // Function to check if ticks would be too close (minimum step difference)
221
+ const getMinStepDifference = (totalSteps, width) => {
222
+ const pixelsPerStep = width / (totalSteps - 1);
223
+ return Math.ceil(minPixelSpacing / pixelsPerStep);
224
+ };
225
+
226
+ const minStepDiff = getMinStepDifference(totalSteps, width);
227
+ const maxPossibleTicks = Math.floor((totalSteps - 1) / minStepDiff) + 1;
228
+
229
+ // Ensure we aim for at least 5 ticks if space permits
230
+ const targetMinTicks = Math.min(Math.max(minTicks, 5), maxPossibleTicks, totalSteps);
231
+ const targetMaxTicks = Math.min(maxTicks, maxTicksFromWidth, maxPossibleTicks);
232
+
233
+ // Start with first and last
234
+ if (targetMinTicks <= 2 || totalSteps <= 2) {
235
+ return [0, totalSteps - 1];
236
+ }
237
+
238
+ // Helper to validate spacing
239
+ const hasValidSpacing = (ticks) => {
240
+ for (let i = 1; i < ticks.length; i++) {
241
+ if (ticks[i] - ticks[i-1] < minStepDiff) return false;
242
+ }
243
+ return true;
244
+ };
245
+
246
+ // Try nice intervals first
247
+ const candidateIntervals = [];
248
+ const niceIntervals = [1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000];
249
+
250
+ for (const interval of niceIntervals) {
251
+ if (interval >= totalSteps) continue;
252
+
253
+ const candidateTicks = [0];
254
+ const firstStepValue = steps[0];
255
+ const lastStepValue = steps[totalSteps - 1];
256
+ const firstNiceValue = Math.ceil(firstStepValue / interval) * interval;
257
+
258
+ // Add ticks for nice step values
259
+ for (let niceValue = firstNiceValue; niceValue < lastStepValue; niceValue += interval) {
260
+ let closestIndex = 0;
261
+ let minDist = Infinity;
262
+ for (let i = 0; i < steps.length; i++) {
263
+ const dist = Math.abs(steps[i] - niceValue);
264
+ if (dist < minDist) {
265
+ minDist = dist;
266
+ closestIndex = i;
267
+ }
268
+ }
269
+
270
+ // Be more permissive with matching (15% instead of 10%)
271
+ if (minDist <= interval * 0.15 && closestIndex > 0 && closestIndex < totalSteps - 1) {
272
+ candidateTicks.push(closestIndex);
273
+ }
274
+ }
275
+
276
+ candidateTicks.push(totalSteps - 1);
277
+ const uniqueTicks = [...new Set(candidateTicks)].sort((a,b) => a-b);
278
+
279
+ if (hasValidSpacing(uniqueTicks) && uniqueTicks.length >= 3) {
280
+ candidateIntervals.push({
281
+ interval: interval,
282
+ ticks: uniqueTicks,
283
+ count: uniqueTicks.length,
284
+ niceness: (interval <= 10 ? 100 : (interval <= 50 ? 50 : (interval <= 100 ? 25 : 10)))
285
+ });
286
+ }
287
+ }
288
+
289
+ // Force generation of ticks if we don't have enough nice ones
290
+ if (candidateIntervals.length === 0 || candidateIntervals.every(c => c.count < targetMinTicks)) {
291
+ // Try multiple approaches to get targetMinTicks
292
+ for (let targetCount = Math.min(targetMinTicks, maxPossibleTicks); targetCount >= 3; targetCount--) {
293
+ // Approach 1: Even distribution
294
+ const evenSpacing = Math.floor((totalSteps - 1) / (targetCount - 1));
295
+ const evenTicks = [];
296
+ for (let i = 0; i < targetCount - 1; i++) {
297
+ evenTicks.push(i * evenSpacing);
298
+ }
299
+ evenTicks.push(totalSteps - 1);
300
+
301
+ if (hasValidSpacing(evenTicks)) {
302
+ candidateIntervals.push({
303
+ interval: evenSpacing,
304
+ ticks: evenTicks,
305
+ count: evenTicks.length,
306
+ niceness: 5 // Medium priority
307
+ });
308
+ break; // Found a good solution
309
+ }
310
+
311
+ // Approach 2: Try to fit exactly targetCount ticks with optimal spacing
312
+ if (targetCount <= maxPossibleTicks) {
313
+ const optimalSpacing = Math.max(minStepDiff, Math.floor((totalSteps - 1) / (targetCount - 1)));
314
+ const spacedTicks = [0];
315
+ let currentPos = 0;
316
+
317
+ for (let i = 1; i < targetCount - 1; i++) {
318
+ currentPos += optimalSpacing;
319
+ if (currentPos < totalSteps - 1) {
320
+ spacedTicks.push(Math.min(currentPos, totalSteps - 1 - minStepDiff));
321
+ }
322
+ }
323
+ spacedTicks.push(totalSteps - 1);
324
+
325
+ const uniqueSpacedTicks = [...new Set(spacedTicks)].sort((a,b) => a-b);
326
+ if (hasValidSpacing(uniqueSpacedTicks) && uniqueSpacedTicks.length >= targetCount - 1) {
327
+ candidateIntervals.push({
328
+ interval: optimalSpacing,
329
+ ticks: uniqueSpacedTicks,
330
+ count: uniqueSpacedTicks.length,
331
+ niceness: 3 // Lower priority than nice intervals
332
+ });
333
+ break;
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ // Absolute fallback
340
+ if (candidateIntervals.length === 0) {
341
+ const middle = Math.floor(totalSteps / 2);
342
+ if (middle !== 0 && middle !== totalSteps - 1 &&
343
+ middle - 0 >= minStepDiff && totalSteps - 1 - middle >= minStepDiff) {
344
+ return [0, middle, totalSteps - 1];
345
+ }
346
+ return [0, totalSteps - 1];
347
+ }
348
+
349
+ // Sort: prioritize having enough ticks, then niceness
350
+ candidateIntervals.sort((a, b) => {
351
+ const aHasEnoughTicks = a.count >= targetMinTicks;
352
+ const bHasEnoughTicks = b.count >= targetMinTicks;
353
+
354
+ // First: prefer solutions with enough ticks
355
+ if (aHasEnoughTicks !== bHasEnoughTicks) {
356
+ return bHasEnoughTicks ? 1 : -1;
357
+ }
358
+
359
+ // Second: prefer nicer intervals
360
+ if (a.niceness !== b.niceness) {
361
+ return b.niceness - a.niceness;
362
+ }
363
+
364
+ // Third: prefer more ticks if both are nice
365
+ return b.count - a.count;
366
+ });
367
+
368
+ return candidateIntervals[0].ticks;
369
+ }
370
+
371
+ /**
372
+ * Applies smoothing to data series using moving average
373
+ * @param {Array} data - Array of {step, value} objects
374
+ * @param {number} windowSize - Size of the smoothing window (default: 5)
375
+ * @returns {Array} Smoothed data series
376
+ */
377
+ export function smoothData(data, windowSize = 5) {
378
+ if (!data || data.length === 0) return data;
379
+ if (data.length < windowSize) return data; // Not enough data to smooth
380
+
381
+ const smoothed = [];
382
+ const halfWindow = Math.floor(windowSize / 2);
383
+
384
+ for (let i = 0; i < data.length; i++) {
385
+ const start = Math.max(0, i - halfWindow);
386
+ const end = Math.min(data.length - 1, i + halfWindow);
387
+
388
+ let sum = 0;
389
+ let count = 0;
390
+
391
+ // Calculate weighted average with more weight to center point
392
+ for (let j = start; j <= end; j++) {
393
+ const distance = Math.abs(j - i);
394
+ const weight = distance === 0 ? 2 : (distance === 1 ? 1.5 : 1); // Center gets more weight
395
+ sum += data[j].value * weight;
396
+ count += weight;
397
+ }
398
+
399
+ smoothed.push({
400
+ step: data[i].step,
401
+ value: sum / count
402
+ });
403
+ }
404
+
405
+ return smoothed;
406
+ }
407
+
408
+ /**
409
+ * Applies smoothing to all runs in metric data
410
+ * @param {Object} metricData - Object with run names as keys and data arrays as values
411
+ * @param {number} windowSize - Size of the smoothing window
412
+ * @returns {Object} Smoothed metric data
413
+ */
414
+ export function smoothMetricData(metricData, windowSize = 5) {
415
+ if (!metricData) return metricData;
416
+
417
+ const smoothedData = {};
418
+ Object.keys(metricData).forEach(runName => {
419
+ smoothedData[runName] = smoothData(metricData[runName], windowSize);
420
+ });
421
+
422
+ return smoothedData;
423
+ }
app/src/components/trackio/data-generator.js ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Data generation utilities for synthetic training data
2
+
3
+ export function generateRunNames(count) {
4
+ const adjectives = [
5
+ 'ancient', 'brave', 'calm', 'clever', 'crimson', 'daring', 'eager', 'fearless',
6
+ 'gentle', 'glossy', 'golden', 'hidden', 'icy', 'jolly', 'lively', 'mighty',
7
+ 'noble', 'proud', 'quick', 'silent', 'swift', 'tiny', 'vivid', 'wild'
8
+ ];
9
+ const nouns = [
10
+ 'river', 'mountain', 'harbor', 'forest', 'valley', 'ocean', 'meadow', 'desert',
11
+ 'island', 'canyon', 'harbor', 'trail', 'summit', 'delta', 'lagoon', 'ridge',
12
+ 'tundra', 'reef', 'plateau', 'prairie', 'grove', 'bay', 'dune', 'cliff'
13
+ ];
14
+ const used = new Set();
15
+ const names = [];
16
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
17
+
18
+ while (names.length < count) {
19
+ const name = `${pick(adjectives)}-${pick(nouns)}-${Math.floor(1 + Math.random() * 7)}`;
20
+ if (!used.has(name)) {
21
+ used.add(name);
22
+ names.push(name);
23
+ }
24
+ }
25
+ return names;
26
+ }
27
+
28
+ export function genCurves(n) {
29
+ const quality = Math.random();
30
+ const good = quality > 0.66;
31
+ const poor = quality < 0.33;
32
+ const l0 = 2.0 + Math.random() * 4.5;
33
+ const targetLoss = good
34
+ ? l0 * (0.12 + Math.random() * 0.12)
35
+ : (poor ? l0 * (0.35 + Math.random() * 0.25) : l0 * (0.22 + Math.random() * 0.16));
36
+
37
+ const phases = 1 + Math.floor(Math.random() * 3);
38
+ const marksSet = new Set();
39
+ while (marksSet.size < phases - 1) {
40
+ marksSet.add(Math.floor((0.25 + Math.random() * 0.5) * (n - 1)));
41
+ }
42
+ const marks = [0, ...Array.from(marksSet).sort((a, b) => a - b), n - 1];
43
+
44
+ let kLoss = 0.02 + Math.random() * 0.08;
45
+ const loss = new Array(n);
46
+
47
+ for (let seg = 0; seg < marks.length - 1; seg++) {
48
+ const a = marks[seg];
49
+ const b = marks[seg + 1] || a + 1;
50
+
51
+ for (let i = a; i <= b; i++) {
52
+ const t = (i - a) / Math.max(1, (b - a));
53
+ const segTarget = targetLoss * Math.pow(0.85, seg);
54
+ let v = l0 * Math.exp(-kLoss * (i + 1));
55
+ v = 0.6 * v + 0.4 * (l0 + (segTarget - l0) * (seg + t) / Math.max(1, (marks.length - 1)));
56
+ const noiseAmp = (0.08 * l0) * (1 - 0.8 * (i / (n - 1)));
57
+ v += (Math.random() * 2 - 1) * noiseAmp;
58
+ if (Math.random() < 0.02) v += 0.15 * l0;
59
+ loss[i] = Math.max(0, v);
60
+ }
61
+ kLoss *= 1.6;
62
+ }
63
+
64
+ const a0 = 0.1 + Math.random() * 0.35;
65
+ const aMax = good
66
+ ? (0.92 + Math.random() * 0.07)
67
+ : (poor ? (0.62 + Math.random() * 0.14) : (0.8 + Math.random() * 0.1));
68
+
69
+ let kAcc = 0.02 + Math.random() * 0.08;
70
+ const acc = new Array(n);
71
+
72
+ for (let i = 0; i < n; i++) {
73
+ let v = aMax - (aMax - a0) * Math.exp(-kAcc * (i + 1));
74
+ const noiseAmp = 0.04 * (1 - 0.8 * (i / (n - 1)));
75
+ v += (Math.random() * 2 - 1) * noiseAmp;
76
+ acc[i] = Math.max(0, Math.min(1, v));
77
+ if (marksSet.has(i)) kAcc *= 1.4;
78
+ }
79
+
80
+ const accGap = 0.02 + Math.random() * 0.06;
81
+ const lossGap = 0.05 + Math.random() * 0.15;
82
+ const accVal = new Array(n);
83
+ const lossVal = new Array(n);
84
+
85
+ let ofStart = Math.floor(((good ? 0.85 : 0.7) + (Math.random() * 0.15 - 0.05)) * (n - 1));
86
+ ofStart = Math.max(Math.floor(0.5 * (n - 1)), Math.min(Math.floor(0.95 * (n - 1)), ofStart));
87
+
88
+ for (let i = 0; i < n; i++) {
89
+ let av = acc[i] - accGap + (Math.random() * 0.06 - 0.03);
90
+ let lv = loss[i] * (1 + lossGap) + (Math.random() * 0.1 - 0.05) * Math.max(1, l0 * 0.2);
91
+ if (i >= ofStart && !poor) {
92
+ const t = (i - ofStart) / Math.max(1, (n - 1 - ofStart));
93
+ av -= 0.03 * t;
94
+ lv += 0.12 * t * loss[i];
95
+ }
96
+ accVal[i] = Math.max(0, Math.min(1, av));
97
+ lossVal[i] = Math.max(0, lv);
98
+ }
99
+
100
+ return { accTrain: acc, lossTrain: loss, accVal, lossVal };
101
+ }
app/src/components/trackio/store.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // Simple store for triggering Trackio actions
2
+ import { writable } from 'svelte/store';
3
+
4
+ export const jitterTrigger = writable(0);
5
+
6
+ export function triggerJitter() {
7
+ jitterTrigger.update(n => n + 1);
8
+ }
app/src/components/trackio/themes/_components.css ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================
2
+ TRACKIO COMPONENT STYLES
3
+ Shared component styling across themes
4
+ ========================= */
5
+
6
+ /* =========================
7
+ CELL COMPONENTS
8
+ ========================= */
9
+
10
+ /* Base cell structure */
11
+ :global(.trackio .cell) {
12
+ border: 1px solid var(--trackio-cell-border);
13
+ border-radius: var(--trackio-border-radius);
14
+ background: var(--trackio-cell-background);
15
+ display: flex;
16
+ flex-direction: column;
17
+ position: relative;
18
+ }
19
+
20
+ /* Cell background layer - hidden by default */
21
+ :global(.trackio .cell-bg) {
22
+ position: absolute;
23
+ inset: 10px;
24
+ pointer-events: none;
25
+ z-index: 1;
26
+ border-radius: 4px;
27
+ display: none;
28
+ }
29
+
30
+ /* Cell corners layer - hidden by default */
31
+ :global(.trackio .cell-corners) {
32
+ position: absolute;
33
+ inset: 6px;
34
+ pointer-events: none;
35
+ z-index: 3;
36
+ display: none;
37
+ opacity: 0.85;
38
+ }
39
+
40
+ :global(.trackio .cell-inner) {
41
+ position: relative;
42
+ z-index: 2;
43
+ padding: var(--trackio-inner-padding);
44
+ display: flex;
45
+ flex-direction: column;
46
+ }
47
+
48
+ :global(.trackio .cell-header) {
49
+ padding: 8px 10px;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ gap: 8px;
54
+ }
55
+
56
+ :global(.trackio .cell-title) {
57
+ font-size: 13px;
58
+ font-weight: var(--trackio-font-weight-bold);
59
+ color: var(--trackio-text-primary);
60
+ font-family: var(--trackio-font-family);
61
+ }
62
+
63
+ :global(.trackio .cell-body) {
64
+ position: relative;
65
+ width: 100%;
66
+ overflow: hidden;
67
+ }
68
+
69
+ :global(.trackio .cell-body svg) {
70
+ max-width: 100%;
71
+ height: auto;
72
+ display: block;
73
+ }
74
+
75
+ /* Wide cell spans full width */
76
+ :global(.trackio__grid .cell--wide) {
77
+ grid-column: 1 / -1;
78
+ }
79
+
80
+ /* =========================
81
+ LEGEND COMPONENTS
82
+ ========================= */
83
+
84
+ .legend-bottom {
85
+ display: flex;
86
+ flex-direction: column;
87
+ align-items: center;
88
+ gap: 6px;
89
+ font-size: 12px;
90
+ text-align: center;
91
+ width: 80%;
92
+ margin: 0 auto;
93
+ color: var(--trackio-legend-text);
94
+ font-family: var(--trackio-font-family);
95
+ }
96
+
97
+ .legend-title {
98
+ font-size: 12px;
99
+ font-weight: var(--trackio-font-weight-bold);
100
+ color: var(--trackio-legend-text);
101
+ }
102
+
103
+ .items {
104
+ display: flex;
105
+ flex-wrap: wrap;
106
+ gap: 8px 14px;
107
+ justify-content: center;
108
+ align-items: center;
109
+ }
110
+
111
+ .item {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ gap: 6px;
115
+ white-space: nowrap;
116
+ cursor: pointer;
117
+ padding: 2px 4px;
118
+ transition: opacity var(--trackio-transition-normal);
119
+ color: var(--trackio-legend-text);
120
+ }
121
+
122
+ .swatch {
123
+ width: 14px;
124
+ height: 14px;
125
+ border-radius: 3px;
126
+ border: 1px solid var(--trackio-legend-swatch-border);
127
+ display: inline-block;
128
+ }
129
+
130
+ /* Ghost hover effect */
131
+ :global(.trackio.hovering .ghost) {
132
+ opacity: 0.2;
133
+ transition: opacity var(--trackio-transition-normal);
134
+ }
135
+
136
+ :global(.trackio.hovering) .item.ghost {
137
+ opacity: 0.2;
138
+ }
139
+
140
+ /* =========================
141
+ TOOLTIP COMPONENTS
142
+ ========================= */
143
+
144
+ .d3-tooltip {
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ transform: translate(-9999px, -9999px);
149
+ pointer-events: none;
150
+ padding: 10px 12px;
151
+ border-radius: 12px;
152
+ font-size: 12px;
153
+ line-height: 1.35;
154
+ border: 1px solid var(--trackio-tooltip-border);
155
+ background: var(--trackio-tooltip-background);
156
+ color: var(--trackio-text-primary);
157
+ box-shadow: var(--trackio-tooltip-shadow);
158
+ opacity: 0;
159
+ transition: opacity var(--trackio-transition-fast);
160
+ z-index: var(--z-tooltip);
161
+ backdrop-filter: saturate(1.12) blur(8px);
162
+ }
163
+
164
+ .d3-tooltip.is-visible {
165
+ opacity: 1;
166
+ }
167
+
168
+ .d3-tooltip__inner {
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 6px;
172
+ min-width: 220px;
173
+ }
174
+
175
+ .d3-tooltip__inner > div:first-child {
176
+ font-weight: var(--trackio-font-weight-extra-bold);
177
+ letter-spacing: 0.1px;
178
+ margin-bottom: 0;
179
+ }
180
+
181
+ .d3-tooltip__inner > div:nth-child(2) {
182
+ font-size: 11px;
183
+ color: var(--trackio-text-secondary);
184
+ display: block;
185
+ margin-top: -4px;
186
+ margin-bottom: 2px;
187
+ letter-spacing: 0.1px;
188
+ }
189
+
190
+ .d3-tooltip__inner > div.row {
191
+ padding-top: 6px;
192
+ border-top: 1px solid var(--trackio-tooltip-border);
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 8px;
196
+ white-space: nowrap;
197
+ }
198
+
199
+ .dot {
200
+ display: inline-block;
201
+ width: 12px;
202
+ height: 12px;
203
+ border-radius: 3px;
204
+ border: 1px solid var(--trackio-tooltip-border);
205
+ }
206
+
207
+ .val {
208
+ margin-left: auto;
209
+ text-align: right;
210
+ }
211
+
212
+ /* =========================
213
+ FULLSCREEN COMPONENTS
214
+ ========================= */
215
+
216
+ /* Fullscreen button */
217
+ .cell-fullscreen-btn {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ width: 32px;
222
+ height: 32px;
223
+ border: 0;
224
+ background: transparent;
225
+ color: var(--trackio-chart-axis-text);
226
+ opacity: 0.6;
227
+ cursor: pointer;
228
+ border-radius: 6px;
229
+ transition: opacity var(--trackio-transition-normal);
230
+ }
231
+
232
+ .cell-fullscreen-btn:hover {
233
+ opacity: 1;
234
+ }
235
+
236
+ .cell-fullscreen-btn svg {
237
+ width: 18px;
238
+ height: 18px;
239
+ fill: var(--trackio-chart-axis-text);
240
+ }
241
+
242
+ /* Fullscreen modal */
243
+ :global(.trackio-fullscreen-overlay) {
244
+ position: fixed;
245
+ inset: 0;
246
+ background: rgba(0, 0, 0, 0.85);
247
+ backdrop-filter: blur(8px);
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ z-index: var(--z-overlay);
252
+ opacity: 0;
253
+ pointer-events: none;
254
+ transition: opacity var(--trackio-transition-slow);
255
+ }
256
+
257
+ :global(.trackio-fullscreen-overlay.is-open) {
258
+ opacity: 1;
259
+ pointer-events: auto;
260
+ }
261
+
262
+ :global(.trackio-fullscreen-modal) {
263
+ position: relative;
264
+ width: min(95vw, 1400px);
265
+ height: min(95vh, 900px);
266
+ background: var(--surface-bg);
267
+ border-radius: 12px;
268
+ overflow: hidden;
269
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
270
+ }
271
+
272
+ :global(.trackio-fullscreen-close) {
273
+ position: absolute;
274
+ top: 16px;
275
+ right: 16px;
276
+ width: 40px;
277
+ height: 40px;
278
+ border: 0;
279
+ background: rgba(0, 0, 0, 0.5);
280
+ color: white;
281
+ border-radius: 50%;
282
+ cursor: pointer;
283
+ display: flex;
284
+ align-items: center;
285
+ justify-content: center;
286
+ z-index: 10;
287
+ font-size: 20px;
288
+ transition: background-color var(--trackio-transition-normal);
289
+ }
290
+
291
+ :global(.trackio-fullscreen-close:hover) {
292
+ background: rgba(0, 0, 0, 0.7);
293
+ }
294
+
295
+ /* Hide fullscreen button when in modal */
296
+ :global(.trackio-fullscreen-modal .cell-fullscreen-btn) {
297
+ display: none;
298
+ }
299
+
300
+ /* Fullscreen cell styling */
301
+ :global(.trackio-fullscreen-modal .cell) {
302
+ width: 100%;
303
+ height: 100%;
304
+ border: none;
305
+ border-radius: 0;
306
+ }
307
+
308
+ :global(.trackio-fullscreen-modal .cell-inner) {
309
+ height: 100%;
310
+ }
311
+
312
+ :global(.trackio-fullscreen-modal .cell-body) {
313
+ flex: 1;
314
+ height: calc(100% - 50px);
315
+ }
316
+
317
+ :global(.trackio-fullscreen-modal .cell-body svg) {
318
+ width: 100% !important;
319
+ height: 100% !important;
320
+ max-width: none !important;
321
+ max-height: none !important;
322
+ }
323
+
324
+ /* =========================
325
+ CHART STYLING
326
+ ========================= */
327
+
328
+ /* Legacy axis/grid selectors - for compatibility */
329
+ .trackio .axes path,
330
+ .trackio .axes line {
331
+ stroke: var(--trackio-chart-axis-stroke);
332
+ }
333
+
334
+ .trackio .axes text {
335
+ fill: var(--trackio-chart-axis-text);
336
+ font-family: var(--trackio-font-family);
337
+ }
338
+
339
+ .trackio .grid line {
340
+ stroke: var(--trackio-chart-grid-stroke);
341
+ opacity: var(--trackio-chart-grid-opacity);
342
+ }
343
+
344
+ /* Minor ticks styling for logarithmic scales */
345
+ :global(.trackio .minor-ticks line.minor-tick) {
346
+ stroke: var(--trackio-chart-axis-stroke);
347
+ stroke-opacity: 0.4;
348
+ stroke-width: 0.5px;
349
+ }
350
+
351
+ /* Grid type switching */
352
+ .trackio .grid-dots {
353
+ display: none;
354
+ }
355
+
356
+ /* =========================
357
+ OBLIVION THEME COMPONENT OVERRIDES
358
+ ========================= */
359
+
360
+ /* Oblivion cell specific styling */
361
+ :global(.trackio.theme--oblivion .cell-inner) {
362
+ padding: var(--trackio-oblivion-hud-corner-size, 8px) 12px 10px var(--trackio-oblivion-hud-gap, 10px);
363
+ }
364
+
365
+ :global(.trackio.theme--oblivion .cell-bg) {
366
+ display: block !important;
367
+ background:
368
+ radial-gradient(1200px 200px at 20% -10%, rgba(0,0,0,.05), transparent 80%),
369
+ radial-gradient(900px 200px at 80% 110%, rgba(0,0,0,.05), transparent 80%);
370
+ }
371
+
372
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion .cell-bg) {
373
+ background:
374
+ radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, #ffffff 6.5%, transparent), transparent 80%),
375
+ radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, #ffffff 6%, transparent), transparent 80%),
376
+ linear-gradient(180deg, color-mix(in srgb, #ffffff 3.5%, transparent), transparent 45%);
377
+ }
378
+
379
+ :global(.trackio.theme--oblivion .cell-corners) {
380
+ display: block !important;
381
+ inset: 6px;
382
+ background:
383
+ linear-gradient(#000000, #000000) top left / 8px 1px no-repeat,
384
+ linear-gradient(#000000, #000000) top left / 1px 8px no-repeat,
385
+ linear-gradient(#000000, #000000) top right / 8px 1px no-repeat,
386
+ linear-gradient(#000000, #000000) top right / 1px 8px no-repeat,
387
+ linear-gradient(#000000, #000000) bottom left / 8px 1px no-repeat,
388
+ linear-gradient(#000000, #000000) bottom left / 1px 8px no-repeat,
389
+ linear-gradient(#000000, #000000) bottom right / 8px 1px no-repeat,
390
+ linear-gradient(#000000, #000000) bottom right / 1px 8px no-repeat;
391
+ opacity: 1;
392
+ z-index: 3;
393
+ }
394
+
395
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion .cell-corners) {
396
+ background:
397
+ linear-gradient(#ffffff, #ffffff) top left / 8px 1px no-repeat,
398
+ linear-gradient(#ffffff, #ffffff) top left / 1px 8px no-repeat,
399
+ linear-gradient(#ffffff, #ffffff) top right / 8px 1px no-repeat,
400
+ linear-gradient(#ffffff, #ffffff) top right / 1px 8px no-repeat,
401
+ linear-gradient(#ffffff, #ffffff) bottom left / 8px 1px no-repeat,
402
+ linear-gradient(#ffffff, #ffffff) bottom left / 1px 8px no-repeat,
403
+ linear-gradient(#ffffff, #ffffff) bottom right / 8px 1px no-repeat,
404
+ linear-gradient(#ffffff, #ffffff) bottom right / 1px 8px no-repeat;
405
+ }
406
+
407
+ :global(.trackio.theme--oblivion .cell-header) {
408
+ padding: 5px 0px 18px 12px;
409
+ }
410
+
411
+ :global(.trackio.theme--oblivion .cell-title) {
412
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
413
+ font-size: 12px;
414
+ font-weight: 800;
415
+ letter-spacing: 0.12em;
416
+ text-transform: uppercase;
417
+ color: var(--trackio-oblivion-primary);
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 8px;
421
+ position: relative;
422
+ padding-left: 0;
423
+ }
424
+
425
+ :global(.trackio.theme--oblivion .cell-title)::before {
426
+ content: "";
427
+ position: absolute;
428
+ left: 0;
429
+ top: 50%;
430
+ transform: translateY(-50%);
431
+ width: 6px;
432
+ height: 6px;
433
+ background: var(--trackio-oblivion-primary);
434
+ border: 1px solid var(--trackio-oblivion-dim);
435
+ box-shadow: 0 0 10px color-mix(in srgb, var(--trackio-oblivion-base) 25%, transparent) inset;
436
+ opacity: 0.5;
437
+ }
438
+
439
+ /* Force Roboto Mono in Oblivion theme for legend */
440
+ :global(.trackio.theme--oblivion) .legend-bottom,
441
+ :global(.trackio.theme--oblivion) .legend-title,
442
+ :global(.trackio.theme--oblivion) .item {
443
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
444
+ letter-spacing: 0.06em;
445
+ }
446
+
447
+ :global(.trackio.theme--oblivion) .legend-title {
448
+ font-weight: 900 !important;
449
+ letter-spacing: 0.18em !important;
450
+ text-transform: uppercase !important;
451
+ }
452
+
453
+ /* Oblivion theme: enhanced minor ticks */
454
+ :global(.trackio.theme--oblivion .minor-ticks line.minor-tick) {
455
+ stroke: var(--trackio-oblivion-dim);
456
+ stroke-opacity: 0.6;
457
+ stroke-width: 0.8px;
458
+ }
459
+
460
+ /* Force font-family for SVG text in Oblivion */
461
+ .trackio.theme--oblivion .axes text {
462
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
463
+ }
464
+
465
+ /* =========================
466
+ OBLIVION TOOLTIP STYLES
467
+ ========================= */
468
+
469
+ /* Tooltip variables for Oblivion theme */
470
+ :global(.trackio.theme--oblivion) .d3-tooltip {
471
+ /* Light mode variables */
472
+ --tooltip-oblivion-base: #2a2a2a;
473
+ --tooltip-oblivion-bg-primary: color-mix(in srgb, var(--tooltip-oblivion-base) 15%, transparent);
474
+ --tooltip-oblivion-bg-secondary: color-mix(in srgb, var(--tooltip-oblivion-base) 10%, transparent);
475
+ --tooltip-oblivion-bg-base: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
476
+ --tooltip-oblivion-border: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
477
+ --tooltip-oblivion-shadow: color-mix(in srgb, var(--tooltip-oblivion-base) 8%, transparent);
478
+ --tooltip-oblivion-text: color-mix(in srgb, var(--tooltip-oblivion-base) 90%, transparent);
479
+ --tooltip-oblivion-line-dark: rgba(0, 0, 0, 0.08);
480
+ --tooltip-oblivion-line-light: rgba(255, 255, 255, 0.15);
481
+ }
482
+
483
+ /* Dark mode variables */
484
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion) .d3-tooltip {
485
+ --tooltip-oblivion-base: #ffffff;
486
+ --tooltip-oblivion-bg-primary: color-mix(in srgb, var(--tooltip-oblivion-base) 15%, transparent);
487
+ --tooltip-oblivion-bg-secondary: color-mix(in srgb, var(--tooltip-oblivion-base) 10%, transparent);
488
+ --tooltip-oblivion-bg-base: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
489
+ --tooltip-oblivion-border: color-mix(in srgb, var(--tooltip-oblivion-base) 5%, transparent);
490
+ --tooltip-oblivion-shadow: color-mix(in srgb, var(--tooltip-oblivion-base) 8%, transparent);
491
+ --tooltip-oblivion-text: color-mix(in srgb, var(--tooltip-oblivion-base) 90%, transparent);
492
+ --tooltip-oblivion-line-dark: rgba(0, 0, 0, 0.15);
493
+ --tooltip-oblivion-line-light: rgba(255, 255, 255, 0.08);
494
+ }
495
+
496
+ /* Oblivion tooltip styling */
497
+ :global(.trackio.theme--oblivion) .d3-tooltip {
498
+ border-radius: 8px;
499
+ border: none;
500
+ background:
501
+ radial-gradient(400px 100px at 30% 0%, var(--tooltip-oblivion-bg-primary), transparent 70%),
502
+ radial-gradient(300px 80px at 70% 100%, var(--tooltip-oblivion-bg-secondary), transparent 70%),
503
+ var(--tooltip-oblivion-bg-base);
504
+ color: var(--tooltip-oblivion-text);
505
+ box-shadow:
506
+ 0 0 0 1px var(--tooltip-oblivion-border),
507
+ 0 8px 40px var(--tooltip-oblivion-shadow),
508
+ 0 2px 8px rgba(0, 0, 0, 0.05);
509
+ backdrop-filter: saturate(1.1) blur(10px);
510
+ opacity: 0.5;
511
+ }
512
+
513
+ :global(.trackio.theme--oblivion) .d3-tooltip.is-visible {
514
+ opacity: 1;
515
+ }
516
+
517
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div:first-child {
518
+ font-weight: 900;
519
+ letter-spacing: 0.18em;
520
+ text-transform: uppercase;
521
+ color: var(--tooltip-oblivion-text);
522
+ }
523
+
524
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div:nth-child(2) {
525
+ color: var(--tooltip-oblivion-text);
526
+ opacity: 0.4;
527
+ letter-spacing: 0.06em;
528
+ }
529
+
530
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div.row {
531
+ position: relative;
532
+ padding-top: 10px;
533
+ margin-top: 6px;
534
+ border-top: none;
535
+ }
536
+
537
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div.row::before {
538
+ content: "";
539
+ position: absolute;
540
+ left: 0;
541
+ right: 0;
542
+ top: 0;
543
+ height: 1px;
544
+ background: var(--tooltip-oblivion-line-dark);
545
+ }
546
+
547
+ :global(.trackio.theme--oblivion) .d3-tooltip__inner > div.row::after {
548
+ content: "";
549
+ position: absolute;
550
+ left: 0;
551
+ right: 0;
552
+ top: 1px;
553
+ height: 1px;
554
+ background: var(--tooltip-oblivion-line-light);
555
+ }
556
+
557
+ :global(.trackio.theme--oblivion) .dot {
558
+ border-radius: 2px;
559
+ border: 1px solid var(--trackio-chart-axis-stroke);
560
+ box-shadow: 0 0 10px color-mix(in srgb, var(--trackio-oblivion-base) 20%, transparent) inset;
561
+ }
app/src/components/trackio/themes/_theme-base.css ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================
2
+ TRACKIO THEME SYSTEM
3
+ Base theme variables and typography
4
+ ========================= */
5
+
6
+ /* Font imports for themes */
7
+ @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700;800;900&display=swap');
8
+
9
+ /* Fallback font-face declaration */
10
+ @font-face {
11
+ font-family: 'Roboto Mono Fallback';
12
+ src: url('https://fonts.gstatic.com/s/robotomono/v23/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW4AJi8SJQt.woff2') format('woff2');
13
+ font-weight: 400;
14
+ font-style: normal;
15
+ font-display: swap;
16
+ }
17
+
18
+ /* Base variables - all themes inherit these */
19
+ .trackio {
20
+ position: relative;
21
+ --z-tooltip: 50;
22
+ --z-overlay: 99999999;
23
+
24
+ /* Typography system */
25
+ --trackio-font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
26
+ --trackio-font-weight-normal: 400;
27
+ --trackio-font-weight-medium: 600;
28
+ --trackio-font-weight-bold: 700;
29
+ --trackio-font-weight-extra-bold: 800;
30
+ --trackio-font-weight-black: 900;
31
+
32
+ /* Apply font-family to root element */
33
+ font-family: var(--trackio-font-family);
34
+
35
+ /* Layout system */
36
+ --trackio-cell-gap: 12px;
37
+ --trackio-border-radius: 10px;
38
+ --trackio-inner-padding: 8px 12px 10px 10px;
39
+
40
+ /* Animation system */
41
+ --trackio-transition-fast: 0.12s ease;
42
+ --trackio-transition-normal: 0.15s ease;
43
+ --trackio-transition-slow: 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
44
+ }
45
+
46
+ /* Dark mode base adjustments */
47
+ :global([data-theme="dark"]) .trackio {
48
+ /* Dark mode base overrides will be defined in individual theme files */
49
+ }
app/src/components/trackio/themes/_theme-classic.css ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================
2
+ TRACKIO THEME: CLASSIC
3
+ Clean, traditional interface theme
4
+ ========================= */
5
+
6
+ .trackio.theme--classic {
7
+ /* Base color system */
8
+ --trackio-base: #323232;
9
+ --trackio-primary: var(--trackio-base);
10
+ --trackio-dim: color-mix(in srgb, var(--trackio-base) 28%, transparent);
11
+ --trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
12
+ --trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
13
+
14
+ /* Chart rendering */
15
+ --trackio-chart-grid-type: 'lines';
16
+ --trackio-chart-axis-stroke: var(--trackio-dim);
17
+ --trackio-chart-axis-text: var(--trackio-text);
18
+ --trackio-chart-grid-stroke: var(--trackio-subtle);
19
+ --trackio-chart-grid-opacity: 1;
20
+
21
+ /* Cell styling */
22
+ --trackio-cell-background: rgba(0, 0, 0, 0.02);
23
+ --trackio-cell-border: var(--border-color, rgba(0, 0, 0, 0.1));
24
+ --trackio-cell-corner-inset: 0px;
25
+
26
+ /* Typography */
27
+ --trackio-text-primary: var(--text-color, rgba(0, 0, 0, 0.9));
28
+ --trackio-text-secondary: var(--muted-color, rgba(0, 0, 0, 0.6));
29
+ --trackio-text-accent: var(--primary-color, #E889AB);
30
+
31
+ /* Tooltip */
32
+ --trackio-tooltip-background: var(--surface-bg, white);
33
+ --trackio-tooltip-border: var(--border-color, rgba(0, 0, 0, 0.1));
34
+ --trackio-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
35
+
36
+ /* Legend */
37
+ --trackio-legend-text: var(--text-color, rgba(0, 0, 0, 0.9));
38
+ --trackio-legend-swatch-border: var(--border-color, rgba(0, 0, 0, 0.1));
39
+ }
40
+
41
+ /* Dark mode overrides for Classic theme */
42
+ :global([data-theme="dark"]) .trackio.theme--classic {
43
+ --trackio-base: #ffffff;
44
+ --trackio-primary: var(--trackio-base);
45
+ --trackio-dim: color-mix(in srgb, var(--trackio-base) 25%, transparent);
46
+ --trackio-text: color-mix(in srgb, var(--trackio-base) 60%, transparent);
47
+ --trackio-subtle: color-mix(in srgb, var(--trackio-base) 8%, transparent);
48
+
49
+ /* Cell background for dark mode */
50
+ --trackio-cell-background: rgba(255, 255, 255, 0.03);
51
+
52
+ /* Chart adjustments */
53
+ --trackio-chart-axis-stroke: rgba(255, 255, 255, 0.18);
54
+ --trackio-chart-axis-text: rgba(255, 255, 255, 0.60);
55
+ --trackio-chart-grid-stroke: rgba(255, 255, 255, 0.08);
56
+ }
57
+
58
+ /* Classic theme cell styling */
59
+ .trackio.theme--classic .cell {
60
+ border: 1px solid var(--trackio-cell-border) !important;
61
+ background: var(--trackio-cell-background) !important;
62
+ border-radius: var(--trackio-border-radius) !important;
63
+ }
64
+
65
+ /* Classic theme ensures borders are visible */
66
+ .trackio.theme--classic .cell-bg,
67
+ .trackio.theme--classic .cell-corners {
68
+ display: none !important;
69
+ }
app/src/components/trackio/themes/_theme-neon.css ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================
2
+ TRACKIO THEME: NEON
3
+ Cyberpunk-inspired neon theme (example)
4
+ ========================= */
5
+
6
+ .trackio.theme--neon {
7
+ /* Neon color system */
8
+ --trackio-neon-base: #00ffff;
9
+ --trackio-neon-secondary: #ff00ff;
10
+ --trackio-neon-accent: #ffff00;
11
+ --trackio-neon-dim: color-mix(in srgb, var(--trackio-neon-base) 40%, transparent);
12
+ --trackio-neon-subtle: color-mix(in srgb, var(--trackio-neon-base) 15%, transparent);
13
+ --trackio-neon-glow: 0 0 20px var(--trackio-neon-base);
14
+
15
+ /* Chart rendering overrides */
16
+ --trackio-chart-grid-type: 'lines';
17
+ --trackio-chart-axis-stroke: var(--trackio-neon-dim);
18
+ --trackio-chart-axis-text: var(--trackio-neon-base);
19
+ --trackio-chart-grid-stroke: var(--trackio-neon-subtle);
20
+ --trackio-chart-grid-opacity: 0.8;
21
+
22
+ /* Cell styling */
23
+ --trackio-cell-background: rgba(0, 0, 0, 0.8);
24
+ --trackio-cell-border: 2px solid var(--trackio-neon-base);
25
+ --trackio-cell-gap: 16px;
26
+
27
+ /* Typography */
28
+ --trackio-font-family: 'Courier New', monospace;
29
+ --trackio-text-primary: var(--trackio-neon-base);
30
+ --trackio-text-secondary: var(--trackio-neon-dim);
31
+ --trackio-text-accent: var(--trackio-neon-secondary);
32
+
33
+ /* Neon glow effects */
34
+ background: #000;
35
+ color: var(--trackio-neon-base);
36
+ text-shadow: var(--trackio-neon-glow);
37
+ }
38
+
39
+ /* Dark mode (enhanced neon) */
40
+ :global([data-theme="dark"]) .trackio.theme--neon {
41
+ --trackio-neon-glow: 0 0 30px var(--trackio-neon-base);
42
+ background: #000010;
43
+ }
44
+
45
+ /* Neon cell effects */
46
+ .trackio.theme--neon .cell {
47
+ border: 2px solid var(--trackio-neon-base) !important;
48
+ box-shadow:
49
+ 0 0 20px color-mix(in srgb, var(--trackio-neon-base) 30%, transparent),
50
+ inset 0 0 20px color-mix(in srgb, var(--trackio-neon-base) 10%, transparent);
51
+ background: rgba(0, 0, 0, 0.9) !important;
52
+ }
53
+
54
+ /* Neon title styling */
55
+ .trackio.theme--neon .cell-title {
56
+ color: var(--trackio-neon-base);
57
+ text-shadow: var(--trackio-neon-glow);
58
+ text-transform: uppercase;
59
+ letter-spacing: 0.1em;
60
+ }
61
+
62
+ /* Neon grid styling */
63
+ .trackio.theme--neon .grid line {
64
+ filter: drop-shadow(0 0 3px var(--trackio-neon-base));
65
+ }
66
+
67
+ /* Neon legend styling */
68
+ .trackio.theme--neon .legend-title {
69
+ color: var(--trackio-neon-secondary);
70
+ text-shadow: 0 0 15px var(--trackio-neon-secondary);
71
+ text-transform: uppercase;
72
+ letter-spacing: 0.15em;
73
+ }
app/src/components/trackio/themes/_theme-oblivion.css ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================
2
+ TRACKIO THEME: OBLIVION
3
+ Futuristic HUD-style interface theme
4
+ ========================= */
5
+
6
+ .trackio.theme--oblivion {
7
+ /* Core oblivion color system - Light mode: darker colors for visibility */
8
+ --trackio-oblivion-base: #2a2a2a;
9
+ --trackio-oblivion-primary: var(--trackio-oblivion-base);
10
+ --trackio-oblivion-dim: color-mix(in srgb, var(--trackio-oblivion-base) 30%, transparent);
11
+ --trackio-oblivion-subtle: color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent);
12
+ --trackio-oblivion-ghost: color-mix(in srgb, var(--trackio-oblivion-base) 4%, transparent);
13
+
14
+ /* Chart rendering overrides */
15
+ --trackio-chart-grid-type: 'dots';
16
+ --trackio-chart-axis-stroke: var(--trackio-oblivion-dim);
17
+ --trackio-chart-axis-text: var(--trackio-oblivion-primary);
18
+ --trackio-chart-grid-stroke: var(--trackio-oblivion-dim);
19
+ --trackio-chart-grid-opacity: 0.6;
20
+
21
+ /* Cell styling overrides */
22
+ --trackio-cell-background: var(--trackio-oblivion-subtle);
23
+ --trackio-cell-border: var(--trackio-oblivion-dim);
24
+ --trackio-cell-corner-inset: 6px;
25
+ --trackio-cell-gap: 0px;
26
+
27
+ /* HUD-specific variables */
28
+ --trackio-oblivion-hud-gap: 10px;
29
+ --trackio-oblivion-hud-corner-size: 8px;
30
+ --trackio-oblivion-hud-bg-gradient:
31
+ radial-gradient(1200px 200px at 20% -10%, var(--trackio-oblivion-ghost), transparent 80%),
32
+ radial-gradient(900px 200px at 80% 110%, var(--trackio-oblivion-ghost), transparent 80%);
33
+
34
+ /* Typography overrides */
35
+ --trackio-font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace;
36
+ --trackio-text-primary: var(--trackio-oblivion-primary);
37
+ --trackio-text-secondary: var(--trackio-oblivion-dim);
38
+ --trackio-text-accent: var(--trackio-oblivion-primary);
39
+
40
+ /* Tooltip overrides */
41
+ --trackio-tooltip-background: var(--trackio-oblivion-subtle);
42
+ --trackio-tooltip-border: var(--trackio-oblivion-dim);
43
+ --trackio-tooltip-shadow:
44
+ 0 8px 32px color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent),
45
+ 0 2px 8px color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent);
46
+
47
+ /* Legend overrides */
48
+ --trackio-legend-text: var(--trackio-oblivion-primary);
49
+ --trackio-legend-swatch-border: var(--trackio-oblivion-dim);
50
+
51
+ /* Force font application */
52
+ font-family: var(--trackio-font-family) !important;
53
+ color: var(--trackio-text-primary);
54
+ }
55
+
56
+ /* Dark mode overrides for Oblivion theme */
57
+ :global([data-theme="dark"]) .trackio.theme--oblivion {
58
+ --trackio-oblivion-base: #ffffff;
59
+ --trackio-oblivion-primary: var(--trackio-oblivion-base);
60
+ --trackio-oblivion-dim: color-mix(in srgb, var(--trackio-oblivion-base) 25%, transparent);
61
+ --trackio-oblivion-subtle: color-mix(in srgb, var(--trackio-oblivion-base) 8%, transparent);
62
+ --trackio-oblivion-ghost: color-mix(in srgb, var(--trackio-oblivion-base) 4%, transparent);
63
+
64
+ --trackio-oblivion-hud-bg-gradient:
65
+ radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, var(--trackio-oblivion-base) 6.5%, transparent), transparent 80%),
66
+ radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, var(--trackio-oblivion-base) 6%, transparent), transparent 80%),
67
+ linear-gradient(180deg, color-mix(in srgb, var(--trackio-oblivion-base) 3.5%, transparent), transparent 45%);
68
+
69
+ --trackio-tooltip-shadow:
70
+ 0 8px 32px color-mix(in srgb, var(--trackio-oblivion-base) 5%, transparent),
71
+ 0 2px 8px color-mix(in srgb, black 10%, transparent);
72
+
73
+ background: #0f1115;
74
+ }
75
+
76
+ /* Force Roboto Mono application in Oblivion theme */
77
+ .trackio.theme--oblivion,
78
+ .trackio.theme--oblivion * {
79
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
80
+ }
81
+
82
+ /* Specific overrides for different elements in Oblivion */
83
+ .trackio.theme--oblivion .cell-title,
84
+ .trackio.theme--oblivion .legend-bottom,
85
+ .trackio.theme--oblivion .legend-title,
86
+ .trackio.theme--oblivion .item {
87
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
88
+ }
89
+
90
+ /* Oblivion theme: remove default cell border */
91
+ .trackio.theme--oblivion .cell {
92
+ border: none !important;
93
+ background: transparent !important;
94
+ }
95
+
96
+ /* Grid type switching for Oblivion */
97
+ .trackio.theme--oblivion .grid {
98
+ display: none;
99
+ }
100
+ .trackio.theme--oblivion .grid-dots {
101
+ display: block;
102
+ }
103
+ .trackio.theme--oblivion .cell-bg,
104
+ .trackio.theme--oblivion .cell-corners {
105
+ display: block;
106
+ }
app/src/components/trackio/themes/index.css ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========================
2
+ TRACKIO THEME SYSTEM
3
+ Main theme entry point
4
+ ========================= */
5
+
6
+ /* Import base theme system */
7
+ @import './_theme-base.css';
8
+
9
+ /* Import component styles */
10
+ @import './_components.css';
11
+
12
+ /* Import individual themes */
13
+ @import './_theme-classic.css';
14
+ @import './_theme-oblivion.css';
15
+ @import './_theme-neon.css';
16
+
17
+ /* =========================
18
+ LAYOUT SYSTEM
19
+ ========================= */
20
+
21
+ .trackio__grid {
22
+ display: grid;
23
+ grid-template-columns: repeat(2, minmax(0, 1fr));
24
+ gap: var(--trackio-cell-gap);
25
+ }
26
+
27
+ @media (max-width: 980px) {
28
+ .trackio__grid {
29
+ grid-template-columns: 1fr;
30
+ }
31
+ }
32
+
33
+ .trackio__header {
34
+ display: flex;
35
+ align-items: flex-start;
36
+ justify-content: center;
37
+ gap: 12px;
38
+ margin: 0 0 10px 0;
39
+ flex-wrap: wrap;
40
+ width: 100%;
41
+ }
app/src/content/assets/data/trackio_demo.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4c3da98a9f857932dd4b39344370c1ca41b9ccfbdd0fdd43bc9752f346219605
3
- size 9635
 
 
 
 
app/src/content/assets/data/trackio_wandb_demo.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:582f65833ae2138f8f1b57d7a3a07726278550fb5eed589e2c02b3e7bf84a02d
3
- size 6244
 
 
 
 
app/src/content/chapters/vibe-coding-charts.mdx CHANGED
@@ -1,5 +1,6 @@
1
  import HtmlEmbed from '../../components/HtmlEmbed.astro';
2
  import Note from '../../components/Note.astro';
 
3
 
4
  ## Vibe coding charts
5
 
@@ -70,15 +71,8 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
70
  <HtmlEmbed src="d3-pie-quad.html" title="d3-pie-quad: Quad donuts by metric" align="center" frameless desc={'Quad view: Answer Tokens, Number of Samples, Number of Turns, Number of Images.'} />
71
  ---
72
  <HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
 
 
73
  ---
74
- <HtmlEmbed
75
- src="d3-trackio.html"
76
- frameless
77
- />
78
- ---
79
- <HtmlEmbed
80
- src="d3-trackio-oblivion.html"
81
- frameless
82
- />
83
- ---
84
 
 
1
  import HtmlEmbed from '../../components/HtmlEmbed.astro';
2
  import Note from '../../components/Note.astro';
3
+ import TrackioWrapper from '../../components/TrackioWrapper.astro';
4
 
5
  ## Vibe coding charts
6
 
 
71
  <HtmlEmbed src="d3-pie-quad.html" title="d3-pie-quad: Quad donuts by metric" align="center" frameless desc={'Quad view: Answer Tokens, Number of Samples, Number of Turns, Number of Images.'} />
72
  ---
73
  <HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
74
+
75
+ ### Trackio
76
  ---
77
+ <TrackioWrapper />
 
 
 
 
 
 
 
 
 
78
 
app/src/content/embeds/d3-trackio-oblivion.html CHANGED
@@ -80,10 +80,11 @@
80
  /* Important: allow tooltip to overflow outside cell bounds */
81
  overflow: visible;
82
  z-index: 0;
 
83
  }
84
  .d3-trackio-oblivion .cell:hover { z-index: 50; }
85
  /* Background and corners are explicit elements for maintainability */
86
- .d3-trackio-oblivion .cell-bg { position: absolute; inset: var(--hud-gap); pointer-events: none; z-index: 1; background: var(--hud-bg-gradient); }
87
  .d3-trackio-oblivion .cell-corners { position: absolute; inset: var(--corner-inset); pointer-events: none; z-index: 3; background:
88
  linear-gradient(var(--obl-cyan), var(--obl-cyan)) top left / var(--hud-corner-size) 1px no-repeat,
89
  linear-gradient(var(--obl-cyan), var(--obl-cyan)) top left / 1px var(--hud-corner-size) no-repeat,
@@ -100,7 +101,7 @@
100
  /* Fullscreen button (icon only, no behavior here) */
101
  .d3-trackio-oblivion .cell-action { padding: 0 !important; margin-left: auto; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 0; background: transparent; color: var(--obl-cyan); opacity: .5; cursor: pointer; z-index: 4; }
102
  .d3-trackio-oblivion .cell-action:hover { opacity: .7; }
103
- .d3-trackio-oblivion .cell-action svg { width: 22px; height: 22px; opacity: .6; margin-left: 5px; }
104
  .d3-trackio-oblivion .cell-action svg, .d3-trackio-oblivion .cell-action svg path { fill: currentColor; stroke: none; }
105
  [data-theme="dark"] .d3-trackio-oblivion .cell-action svg { opacity: .85; }
106
  .d3-trackio-oblivion .cell-body { position: relative; width: 100%; overflow: hidden; }
@@ -115,7 +116,7 @@
115
  .d3-trackio-oblivion__header { display: flex; align-items: flex-start; justify-content: center; gap: 14px; margin: 0 0 10px 0; flex-wrap: wrap; width: 100%; }
116
  .d3-trackio-oblivion__header .legend-bottom { display: flex; flex-direction: column; align-items: center; gap: 6px; font-size: 12px; color: var(--obl-cyan); text-align: center; }
117
  .d3-trackio-oblivion__header .legend-bottom .legend-title { font-size: 11px; font-weight: 900; letter-spacing: 0.18em; color: var(--obl-cyan); text-transform: uppercase; }
118
- .d3-trackio-oblivion__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
119
  .d3-trackio-oblivion__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; color: var(--obl-cyan); }
120
  .d3-trackio-oblivion__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 2px; border: 1px solid var(--ghost-obl-border); display: inline-block; box-shadow: 0 0 12px color-mix(in srgb, var(--obl-base) 18%, transparent) inset; }
121
 
@@ -250,7 +251,35 @@
250
  const xTicksForced = (Array.isArray(xTicksArg) && xTicksArg.length)
251
  ? Array.from({ length: xTicksArg.length }, (_, i) => i)
252
  : makeTicks(xScale, 8);
253
- const yCount = Math.max(2, Math.min(6, xTicksForced.length));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  const yDom = yScale.domain();
255
  const yTicksForced = (yCount <= 2)
256
  ? [yDom[0], yDom[1]]
@@ -260,7 +289,7 @@
260
  .attr('transform', `translate(0,${innerHeight})`)
261
  .call(
262
  d3.axisBottom(xScale)
263
- .tickValues(xTicksForced)
264
  .tickFormat((i) => {
265
  const val = (Array.isArray(xTicksArg) && xTicksArg[i] != null ? xTicksArg[i] : i);
266
  return formatAbbrev(val);
@@ -304,7 +333,7 @@
304
  .style('text-transform','uppercase')
305
  .text('Steps');
306
  // Y-axis label removed to gain horizontal space
307
- return { innerWidth, innerHeight, xTicksForced, yTicksForced };
308
  }
309
 
310
  function render(metricData, colorForRun){
@@ -317,8 +346,9 @@
317
  runs.forEach(r => { (metricData[r]||[]).forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); minVal = Math.min(minVal, pt.value); maxVal = Math.max(maxVal, pt.value); }); });
318
  if (!isFinite(minStep) || !isFinite(maxStep)) return;
319
  const isAccuracy = /accuracy/i.test(metricKey);
 
320
  const axisLabelY = prettyMetricLabel(metricKey);
321
- if (isAccuracy) yScale.domain([0,1]).nice(); else yScale.domain([minVal, maxVal]).nice();
322
  // Compute unique x steps and build index mapping
323
  const rawSteps = [];
324
  runs.forEach(r => (metricData[r]||[]).forEach(pt => rawSteps.push(pt.step)));
@@ -329,14 +359,21 @@
329
  xScale = d3.scaleLinear().domain([0, Math.max(0, indices.length - 1)]);
330
  // Update line generator X accessor to use index directly
331
  lineGen.x(d => xScale(stepIndex.get(d.step)));
 
 
332
  const { innerWidth, innerHeight, xTicksForced, yTicksForced } = updateLayout(axisLabelY, hoverSteps);
333
 
334
- // Grid as small dots at intersections of y ticks × step positions
335
  // Exclude dots that fall on the origin axes lines (left Y-axis and bottom X-axis)
336
  const gridPoints = [];
337
  const yDomAll = yScale.domain();
338
  const yMin = Array.isArray(yDomAll) ? yDomAll[0] : null;
339
- xTicksForced.forEach(i => {
 
 
 
 
 
340
  yTicksForced.forEach(t => {
341
  if (i !== 0 && (yMin == null || t !== yMin)) {
342
  gridPoints.push({ sx: i, ty: t });
@@ -368,15 +405,16 @@
368
  const ptsSel = gPoints.selectAll('circle.pt').data(allPts, d=>`${d.run}-${d.step}`);
369
  ptsSel.enter().append('circle')
370
  .attr('class','pt')
371
- .attr('r', 2)
372
  .attr('fill', d=>d.color)
373
  .attr('stroke','none')
374
  .attr('cx', d=> xScale(stepIndex.get(d.step)))
375
- .attr('cy', d=>yScale(d.value))
376
  .merge(ptsSel)
377
  .transition().duration(150)
378
  .attr('cx', d=> xScale(stepIndex.get(d.step)))
379
- .attr('cy', d=>yScale(d.value));
 
380
  ptsSel.exit().remove();
381
  // Steps used for hover snapping (unique data steps)
382
  // already computed above as hoverSteps
@@ -400,14 +438,14 @@
400
  cell.__syncAttached = true;
401
  }
402
  function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const idx = Math.round(Math.max(0, Math.min(hoverSteps.length-1, xScale.invert(mx)))); const nearest = hoverSteps[idx]; const xpx = xScale(idx); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-step', { detail: { step: nearest } })); } catch(_){} } let html = `<div>Step ${formatAbbrev(nearest)}</div><div>${prettyMetricLabel(metricKey)}</div>`; const entries = series.map(s=>{ const m = new Map(s.values.map(v=>[v.step, v])); const pt = m.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null); entries.sort((a,b)=> (a.pt.value - b.pt.value)); const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4)); entries.forEach(e => { html += `<div style=\"display:flex;align-items:center;gap:8px;white-space:nowrap;\"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}</span></div>`; }); tipInner.innerHTML = html; const cssVars = getComputedStyle(cell); const offx = Math.max(0, parseFloat(cssVars.getPropertyValue('--tip-offset-x')) || 0); const offy = Math.max(0, parseFloat(cssVars.getPropertyValue('--tip-offset-y')) || 0); const cellRect = cell.getBoundingClientRect(); const cx = (ev && ev.clientX != null) ? ev.clientX : (cellRect.left + mx); const cy = (ev && ev.clientY != null) ? ev.clientY : (cellRect.top + my); const x = cx - cellRect.left + offx; const y = cy - cellRect.top + offy; tip.classList.add('is-visible'); tip.style.transform=`translate(${x}px, ${y}px)`;
403
- // Animate points at the hovered step to grow slightly
404
  try {
405
  gPoints.selectAll('circle.pt')
406
  .transition().duration(140).ease(d3.easeCubicOut)
407
- .attr('r', d => (d && d.step === nearest ? 4 : 2));
408
  } catch(_) {}
409
  }
410
- function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.classList.remove('is-visible'); tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_){} } try { gPoints.selectAll('circle.pt').transition().duration(150).ease(d3.easeCubicOut).attr('r', 2); } catch(_) {} }, 100); }
411
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
412
  }
413
 
@@ -467,15 +505,23 @@
467
  const metricsToDraw = TARGET_METRICS.filter(t => !!TARGET_TO_DATA[t]);
468
 
469
  const runList = Array.from(new Set(cleanRows.map(r => String(r.run||'').trim()).filter(Boolean))).sort();
470
- const palette = (() => {
471
- try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') return window.ColorPalettes.getColors('categorical', runList.length); } catch(_) {}
 
472
  const d = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#7ff1ff','#5ae0e8','#3cc2da','#289ab8','#1a7e9a','#0f637b','#0a4b5e','#083948','#062a36','#041e27'];
473
  return d;
474
  })();
475
- const colorForRun = (name) => palette[runList.indexOf(name) % palette.length];
476
 
477
  const legendItemsHost = legend.querySelector('.items');
478
- legendItemsHost.innerHTML = runList.map((name) => { const color = colorForRun(name); return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`; }).join('');
 
 
 
 
 
 
 
479
 
480
  const dataByMetric = new Map();
481
  metricsToDraw.forEach(tgt => { const m = TARGET_TO_DATA[tgt]; const map = {}; runList.forEach(r => map[r] = []); cleanRows.filter(r=>r.metric===m).forEach(r => { map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr }); }); dataByMetric.set(tgt, map); });
@@ -484,11 +530,133 @@
484
 
485
  const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
486
  if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
 
488
- legendItemsHost.querySelectorAll('.item').forEach(el => {
489
- el.addEventListener('mouseenter', () => { const run = el.getAttribute('data-run'); if (!run) return; host.classList.add('hovering'); host.querySelectorAll('.cell').forEach(cell => { cell.querySelectorAll('.lines path.run').forEach(p => p.classList.toggle('ghost', (p.__data__ && p.__data__.run) !== run)); cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run)); cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run)); }); legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run)); });
490
- el.addEventListener('mouseleave', () => { host.classList.remove('hovering'); host.querySelectorAll('.cell').forEach(cell => { cell.querySelectorAll('.lines path.run').forEach(p => p.classList.remove('ghost')); cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost')); cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost')); }); legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost')); });
491
- });
492
  } catch (e) {
493
  const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e); pre.style.color = 'var(--obl-cyan)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; host.appendChild(pre);
494
  }
 
80
  /* Important: allow tooltip to overflow outside cell bounds */
81
  overflow: visible;
82
  z-index: 0;
83
+
84
  }
85
  .d3-trackio-oblivion .cell:hover { z-index: 50; }
86
  /* Background and corners are explicit elements for maintainability */
87
+ .d3-trackio-oblivion .cell-bg { position: absolute; inset: var(--hud-gap); pointer-events: none; z-index: 1; background: var(--hud-bg-gradient); border-radius: 4px!important;}
88
  .d3-trackio-oblivion .cell-corners { position: absolute; inset: var(--corner-inset); pointer-events: none; z-index: 3; background:
89
  linear-gradient(var(--obl-cyan), var(--obl-cyan)) top left / var(--hud-corner-size) 1px no-repeat,
90
  linear-gradient(var(--obl-cyan), var(--obl-cyan)) top left / 1px var(--hud-corner-size) no-repeat,
 
101
  /* Fullscreen button (icon only, no behavior here) */
102
  .d3-trackio-oblivion .cell-action { padding: 0 !important; margin-left: auto; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 0; background: transparent; color: var(--obl-cyan); opacity: .5; cursor: pointer; z-index: 4; }
103
  .d3-trackio-oblivion .cell-action:hover { opacity: .7; }
104
+ .d3-trackio-oblivion .cell-action svg { width: 22px; height: 22px; opacity: .6; margin-left: 5px; fill: var(--interaction-color); }
105
  .d3-trackio-oblivion .cell-action svg, .d3-trackio-oblivion .cell-action svg path { fill: currentColor; stroke: none; }
106
  [data-theme="dark"] .d3-trackio-oblivion .cell-action svg { opacity: .85; }
107
  .d3-trackio-oblivion .cell-body { position: relative; width: 100%; overflow: hidden; }
 
116
  .d3-trackio-oblivion__header { display: flex; align-items: flex-start; justify-content: center; gap: 14px; margin: 0 0 10px 0; flex-wrap: wrap; width: 100%; }
117
  .d3-trackio-oblivion__header .legend-bottom { display: flex; flex-direction: column; align-items: center; gap: 6px; font-size: 12px; color: var(--obl-cyan); text-align: center; }
118
  .d3-trackio-oblivion__header .legend-bottom .legend-title { font-size: 11px; font-weight: 900; letter-spacing: 0.18em; color: var(--obl-cyan); text-transform: uppercase; }
119
+ .d3-trackio-oblivion__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; justify-content: center; align-items: center; }
120
  .d3-trackio-oblivion__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; color: var(--obl-cyan); }
121
  .d3-trackio-oblivion__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 2px; border: 1px solid var(--ghost-obl-border); display: inline-block; box-shadow: 0 0 12px color-mix(in srgb, var(--obl-base) 18%, transparent) inset; }
122
 
 
251
  const xTicksForced = (Array.isArray(xTicksArg) && xTicksArg.length)
252
  ? Array.from({ length: xTicksArg.length }, (_, i) => i)
253
  : makeTicks(xScale, 8);
254
+ // Adapt tick density to available space (fewer ticks when range is large), prioritizing nice numeric values (1,2,5×10^k)
255
+ const maxXTicks = Math.max(3, Math.min(12, Math.floor(innerWidth / 90)));
256
+ let xTickVals = xTicksForced;
257
+ if (Array.isArray(xTicksArg) && xTicksArg.length) {
258
+ const realMin = xTicksArg[0];
259
+ const realMax = xTicksArg[xTicksArg.length - 1];
260
+ const niceVals = d3.ticks(realMin, realMax, maxXTicks);
261
+ const nearestIndex = (val) => {
262
+ let bestIdx = 0; let bestDist = Infinity;
263
+ for (let i = 0; i < xTicksArg.length; i++) {
264
+ const d = Math.abs(xTicksArg[i] - val);
265
+ if (d < bestDist) { bestDist = d; bestIdx = i; }
266
+ }
267
+ return bestIdx;
268
+ };
269
+ const indices = Array.from(new Set(niceVals.map(v => nearestIndex(v)))).sort((a,b)=>a-b);
270
+ // Ensure edges included
271
+ if (indices[0] !== 0) indices.unshift(0);
272
+ const lastIdx = xTicksArg.length - 1;
273
+ if (indices[indices.length - 1] !== lastIdx) indices.push(lastIdx);
274
+ xTickVals = indices;
275
+ } else if (xTicksForced.length > maxXTicks) {
276
+ const stride = Math.max(1, Math.ceil(xTicksForced.length / maxXTicks));
277
+ const last = xTicksForced[xTicksForced.length - 1];
278
+ xTickVals = xTicksForced.filter((i, idx) => (idx % stride) === 0);
279
+ if (xTickVals[xTickVals.length - 1] !== last) xTickVals.push(last);
280
+ }
281
+ const maxYTicks = Math.max(5, Math.min(6, Math.floor(innerHeight / 60)));
282
+ const yCount = maxYTicks;
283
  const yDom = yScale.domain();
284
  const yTicksForced = (yCount <= 2)
285
  ? [yDom[0], yDom[1]]
 
289
  .attr('transform', `translate(0,${innerHeight})`)
290
  .call(
291
  d3.axisBottom(xScale)
292
+ .tickValues(xTickVals)
293
  .tickFormat((i) => {
294
  const val = (Array.isArray(xTicksArg) && xTicksArg[i] != null ? xTicksArg[i] : i);
295
  return formatAbbrev(val);
 
333
  .style('text-transform','uppercase')
334
  .text('Steps');
335
  // Y-axis label removed to gain horizontal space
336
+ return { innerWidth, innerHeight, xTicksForced: xTickVals, yTicksForced };
337
  }
338
 
339
  function render(metricData, colorForRun){
 
346
  runs.forEach(r => { (metricData[r]||[]).forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); minVal = Math.min(minVal, pt.value); maxVal = Math.max(maxVal, pt.value); }); });
347
  if (!isFinite(minStep) || !isFinite(maxStep)) return;
348
  const isAccuracy = /accuracy/i.test(metricKey);
349
+ const isLoss = /loss/i.test(metricKey);
350
  const axisLabelY = prettyMetricLabel(metricKey);
351
+ if (isAccuracy) yScale.domain([0,1]).nice(); else if (isLoss) yScale.domain([0,1]).nice(); else yScale.domain([minVal, maxVal]).nice();
352
  // Compute unique x steps and build index mapping
353
  const rawSteps = [];
354
  runs.forEach(r => (metricData[r]||[]).forEach(pt => rawSteps.push(pt.step)));
 
359
  xScale = d3.scaleLinear().domain([0, Math.max(0, indices.length - 1)]);
360
  // Update line generator X accessor to use index directly
361
  lineGen.x(d => xScale(stepIndex.get(d.step)));
362
+ const normalizeY = (v) => (isLoss ? ((maxVal > minVal) ? (v - minVal) / (maxVal - minVal) : 0) : v);
363
+ lineGen.y(d => yScale(normalizeY(d.value)));
364
  const { innerWidth, innerHeight, xTicksForced, yTicksForced } = updateLayout(axisLabelY, hoverSteps);
365
 
366
+ // Grid as small dots at intersections of y ticks × step positions (X density relative to number of data steps)
367
  // Exclude dots that fall on the origin axes lines (left Y-axis and bottom X-axis)
368
  const gridPoints = [];
369
  const yDomAll = yScale.domain();
370
  const yMin = Array.isArray(yDomAll) ? yDomAll[0] : null;
371
+ const desiredCols = 24; // target grid columns; density scales with data size via stride
372
+ const xGridStride = Math.max(1, Math.ceil(hoverSteps.length / desiredCols));
373
+ const xGridIdx = [];
374
+ for (let idx = 0; idx < hoverSteps.length; idx += xGridStride) xGridIdx.push(idx);
375
+ if (xGridIdx[xGridIdx.length - 1] !== hoverSteps.length - 1) xGridIdx.push(hoverSteps.length - 1);
376
+ xGridIdx.forEach(i => {
377
  yTicksForced.forEach(t => {
378
  if (i !== 0 && (yMin == null || t !== yMin)) {
379
  gridPoints.push({ sx: i, ty: t });
 
405
  const ptsSel = gPoints.selectAll('circle.pt').data(allPts, d=>`${d.run}-${d.step}`);
406
  ptsSel.enter().append('circle')
407
  .attr('class','pt')
408
+ .attr('r', 0) /* default invisible */
409
  .attr('fill', d=>d.color)
410
  .attr('stroke','none')
411
  .attr('cx', d=> xScale(stepIndex.get(d.step)))
412
+ .attr('cy', d=>yScale(normalizeY(d.value)))
413
  .merge(ptsSel)
414
  .transition().duration(150)
415
  .attr('cx', d=> xScale(stepIndex.get(d.step)))
416
+ .attr('cy', d=>yScale(normalizeY(d.value)))
417
+ .attr('r', 0); /* keep baseline at 0 when not hovering */
418
  ptsSel.exit().remove();
419
  // Steps used for hover snapping (unique data steps)
420
  // already computed above as hoverSteps
 
438
  cell.__syncAttached = true;
439
  }
440
  function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const idx = Math.round(Math.max(0, Math.min(hoverSteps.length-1, xScale.invert(mx)))); const nearest = hoverSteps[idx]; const xpx = xScale(idx); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-step', { detail: { step: nearest } })); } catch(_){} } let html = `<div>Step ${formatAbbrev(nearest)}</div><div>${prettyMetricLabel(metricKey)}</div>`; const entries = series.map(s=>{ const m = new Map(s.values.map(v=>[v.step, v])); const pt = m.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null); entries.sort((a,b)=> (a.pt.value - b.pt.value)); const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4)); entries.forEach(e => { html += `<div style=\"display:flex;align-items:center;gap:8px;white-space:nowrap;\"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}</span></div>`; }); tipInner.innerHTML = html; const cssVars = getComputedStyle(cell); const offx = Math.max(0, parseFloat(cssVars.getPropertyValue('--tip-offset-x')) || 0); const offy = Math.max(0, parseFloat(cssVars.getPropertyValue('--tip-offset-y')) || 0); const cellRect = cell.getBoundingClientRect(); const cx = (ev && ev.clientX != null) ? ev.clientX : (cellRect.left + mx); const cy = (ev && ev.clientY != null) ? ev.clientY : (cellRect.top + my); const x = cx - cellRect.left + offx; const y = cy - cellRect.top + offy; tip.classList.add('is-visible'); tip.style.transform=`translate(${x}px, ${y}px)`;
441
+ // Animate points: from 0 to hover size only for the hovered step
442
  try {
443
  gPoints.selectAll('circle.pt')
444
  .transition().duration(140).ease(d3.easeCubicOut)
445
+ .attr('r', d => (d && d.step === nearest ? 4 : 0));
446
  } catch(_) {}
447
  }
448
+ function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.classList.remove('is-visible'); tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_){} } try { gPoints.selectAll('circle.pt').transition().duration(150).ease(d3.easeCubicOut).attr('r', 0); } catch(_) {} }, 100); }
449
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
450
  }
451
 
 
505
  const metricsToDraw = TARGET_METRICS.filter(t => !!TARGET_TO_DATA[t]);
506
 
507
  const runList = Array.from(new Set(cleanRows.map(r => String(r.run||'').trim()).filter(Boolean))).sort();
508
+ let currentRunList = runList.slice();
509
+ let palette = (() => {
510
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') return window.ColorPalettes.getColors('categorical', currentRunList.length); } catch(_) {}
511
  const d = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#7ff1ff','#5ae0e8','#3cc2da','#289ab8','#1a7e9a','#0f637b','#0a4b5e','#083948','#062a36','#041e27'];
512
  return d;
513
  })();
514
+ const colorForRun = (name) => palette[currentRunList.indexOf(name) % palette.length];
515
 
516
  const legendItemsHost = legend.querySelector('.items');
517
+ function rebuildLegend(){
518
+ legendItemsHost.innerHTML = currentRunList.map((name) => { const color = colorForRun(name); return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`; }).join('');
519
+ legendItemsHost.querySelectorAll('.item').forEach(el => {
520
+ el.addEventListener('mouseenter', () => { const run = el.getAttribute('data-run'); if (!run) return; host.classList.add('hovering'); host.querySelectorAll('.cell').forEach(cell => { cell.querySelectorAll('.lines path.run').forEach(p => p.classList.toggle('ghost', (p.__data__ && p.__data__.run) !== run)); cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run)); cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run)); }); legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run)); });
521
+ el.addEventListener('mouseleave', () => { host.classList.remove('hovering'); host.querySelectorAll('.cell').forEach(cell => { cell.querySelectorAll('.lines path.run').forEach(p => p.classList.remove('ghost')); cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost')); cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost')); }); legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost')); });
522
+ });
523
+ }
524
+ rebuildLegend();
525
 
526
  const dataByMetric = new Map();
527
  metricsToDraw.forEach(tgt => { const m = TARGET_TO_DATA[tgt]; const map = {}; runList.forEach(r => map[r] = []); cleanRows.filter(r=>r.metric===m).forEach(r => { map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr }); }); dataByMetric.set(tgt, map); });
 
530
 
531
  const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
532
  if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
533
+ // Cycle through step lengths: 0=short,1=medium,2=long
534
+ let cycleIdx = 2;
535
+
536
+ // Randomize data slightly on 'r' key press (in-place jitter)
537
+ function jitterData(){
538
+ const mList = (metricsToDraw && metricsToDraw.length) ? metricsToDraw : TARGET_METRICS;
539
+ mList.forEach((tgt) => {
540
+ if (tgt === 'epoch') return; // keep epoch intact
541
+ const map = dataByMetric.get(tgt);
542
+ if (!map) return;
543
+ const isAcc = /acc/i.test(tgt);
544
+ const isLoss = /loss/i.test(tgt);
545
+ const scale = isAcc ? 0.01 : (isLoss ? 0.03 : 0.02);
546
+ Object.keys(map).forEach((run) => {
547
+ map[run].forEach((pt) => {
548
+ const base = Math.abs(pt.value) || 1;
549
+ const delta = (Math.random() * 2 - 1) * scale * base;
550
+ let nv = pt.value + delta;
551
+ if (isAcc) nv = Math.max(0, Math.min(1, nv));
552
+ pt.value = nv;
553
+ });
554
+ });
555
+ });
556
+ }
557
+ // Simulate a fresh dataset on 's' key press (realistic-ish patterns)
558
+ function generateRunNames(count){
559
+ const adjectives = ['ancient','brave','calm','clever','crimson','daring','eager','fearless','gentle','glossy','golden','hidden','icy','jolly','lively','mighty','noble','proud','quick','silent','swift','tiny','vivid','wild'];
560
+ const nouns = ['river','mountain','harbor','forest','valley','ocean','meadow','desert','island','canyon','harbor','trail','summit','delta','lagoon','ridge','tundra','reef','plateau','prairie','grove','bay','dune','cliff'];
561
+ const used = new Set(); const names = [];
562
+ const pick = (arr) => arr[Math.floor(Math.random()*arr.length)];
563
+ while (names.length < count) { const name = `${pick(adjectives)}-${pick(nouns)}-${Math.floor(1+Math.random()*9)}`; if (!used.has(name)) { used.add(name); names.push(name); } }
564
+ return names;
565
+ }
566
+ function simulateData(){
567
+ // Randomize number of runs between 2 and 7 with W&B-like names
568
+ const wantRuns = Math.max(2, Math.floor(2 + Math.random()*6));
569
+ const runsSim = generateRunNames(wantRuns);
570
+ const rnd = (min,max)=> Math.floor(min + Math.random()*(max-min+1));
571
+ // Cycle: short -> medium -> long -> repeat
572
+ let stepsCount = 16;
573
+ if (cycleIdx === 0) stepsCount = rnd(4, 12);
574
+ else if (cycleIdx === 1) stepsCount = rnd(16, 48);
575
+ else stepsCount = rnd(80, 240);
576
+ cycleIdx = (cycleIdx + 1) % 3;
577
+ const steps = Array.from({length: stepsCount}, (_,i)=> i+1);
578
+ function genCurves(n){
579
+ const quality = Math.random(); const good = quality > 0.66; const poor = quality < 0.33;
580
+ // Train loss with LR phases and decaying noise
581
+ const l0 = 2.0 + Math.random()*4.5;
582
+ const targetLoss = good ? l0*(0.12+Math.random()*0.12) : (poor ? l0*(0.35+Math.random()*0.25) : l0*(0.22+Math.random()*0.16));
583
+ const phases = 1 + Math.floor(Math.random()*3);
584
+ const marksSet = new Set();
585
+ while (marksSet.size < phases-1) { marksSet.add(Math.floor((0.25+Math.random()*0.5)*(n-1))); }
586
+ const marks = [0, ...Array.from(marksSet).sort((a,b)=>a-b), n-1];
587
+ let kLoss = 0.02 + Math.random()*0.08; const loss = new Array(n);
588
+ for (let seg=0; seg<marks.length-1; seg++){
589
+ const a=marks[seg], b=marks[seg+1]||a+1;
590
+ for (let i=a;i<=b;i++){
591
+ const t = (i-a)/Math.max(1,(b-a));
592
+ const segTarget = targetLoss * Math.pow(0.85, seg);
593
+ let v = l0*Math.exp(-kLoss*(i+1));
594
+ v = 0.6*v + 0.4*(l0 + (segTarget - l0)*(seg + t)/Math.max(1,(marks.length-1)));
595
+ const noiseAmp = (0.08*l0) * (1 - 0.8*(i/(n-1)));
596
+ v += (Math.random()*2-1)*noiseAmp; if (Math.random() < 0.02) v += 0.15*l0;
597
+ loss[i] = Math.max(0, v);
598
+ }
599
+ kLoss *= 1.6;
600
+ }
601
+ // Train accuracy logistic rise with decaying noise and phase boosts
602
+ const a0 = 0.1 + Math.random()*0.35; const aMax = good ? (0.92+Math.random()*0.07) : (poor ? (0.62+Math.random()*0.14) : (0.8+Math.random()*0.1));
603
+ let kAcc = 0.02 + Math.random()*0.08; const acc = new Array(n);
604
+ for (let i=0;i<n;i++){
605
+ let v = aMax - (aMax - a0)*Math.exp(-kAcc*(i+1));
606
+ const noiseAmp = 0.04*(1 - 0.8*(i/(n-1)));
607
+ v += (Math.random()*2-1)*noiseAmp; acc[i] = Math.max(0, Math.min(1, v));
608
+ if (marksSet.has(i)) kAcc *= 1.4;
609
+ }
610
+ // Validation: gap + noise + optional overfitting
611
+ const accGap = 0.02 + Math.random()*0.06; const lossGap = 0.05 + Math.random()*0.15;
612
+ const accVal = new Array(n); const lossVal = new Array(n);
613
+ let ofStart = Math.floor(((good ? 0.85 : 0.7) + (Math.random()*0.15 - 0.05))*(n-1));
614
+ ofStart = Math.max(Math.floor(0.5*(n-1)), Math.min(Math.floor(0.95*(n-1)), ofStart));
615
+ for (let i=0;i<n;i++){
616
+ let av = acc[i] - accGap + (Math.random()*0.06 - 0.03);
617
+ let lv = loss[i]*(1+lossGap) + (Math.random()*0.1 - 0.05)*Math.max(1, l0*0.2);
618
+ if (i>=ofStart && !poor){ const t = (i-ofStart)/Math.max(1,(n-1-ofStart)); av -= 0.03*t; lv += 0.12*t*loss[i]; }
619
+ accVal[i] = Math.max(0, Math.min(1, av)); lossVal[i] = Math.max(0, lv);
620
+ }
621
+ return { accTrain: acc, lossTrain: loss, accVal, lossVal };
622
+ }
623
+ // Build fresh maps per metric
624
+ const nextByMetric = new Map();
625
+ const mList = (metricsToDraw && metricsToDraw.length) ? metricsToDraw : TARGET_METRICS;
626
+ mList.forEach(tgt => {
627
+ const map = {}; runsSim.forEach(r => map[r] = []); nextByMetric.set(tgt, map);
628
+ });
629
+ runsSim.forEach(run => {
630
+ const curves = genCurves(stepsCount);
631
+ steps.forEach((s,i)=>{
632
+ if (metricsToDraw.includes('epoch')) nextByMetric.get('epoch')[run].push({ step:s, value:s });
633
+ if (metricsToDraw.includes('train_accuracy')) nextByMetric.get('train_accuracy')[run].push({ step:s, value: curves.accTrain[i] });
634
+ if (metricsToDraw.includes('val_accuracy')) nextByMetric.get('val_accuracy')[run].push({ step:s, value: curves.accVal[i] });
635
+ if (metricsToDraw.includes('train_loss')) nextByMetric.get('train_loss')[run].push({ step:s, value: curves.lossTrain[i] });
636
+ if (metricsToDraw.includes('val_loss')) nextByMetric.get('val_loss')[run].push({ step:s, value: curves.lossVal[i] });
637
+ });
638
+ });
639
+ // Swap data
640
+ nextByMetric.forEach((v,k)=> dataByMetric.set(k,v));
641
+ // Update current run list, palette and legend
642
+ currentRunList = runsSim.slice();
643
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') palette = window.ColorPalettes.getColors('categorical', currentRunList.length); } catch(_) {}
644
+ if (!palette) { palette = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#7ff1ff','#5ae0e8','#3cc2da','#289ab8','#1a7e9a','#0f637b','#0a4b5e','#083948','#062a36','#041e27']; }
645
+ rebuildLegend();
646
+ }
647
+ const onKeyDownJitter = (ev) => {
648
+ try {
649
+ const key = ev && ev.key ? ev.key.toLowerCase() : '';
650
+ if (key === 'r') { jitterData(); rerender(); }
651
+ if (key === 's') { simulateData(); rerender(); }
652
+ } catch(_) {}
653
+ };
654
+ window.addEventListener('keydown', onKeyDownJitter);
655
+ // Start with level 3 (long) synthetic data by default
656
+ simulateData();
657
+ rerender();
658
 
659
+ // (Legend hover handlers bound inside rebuildLegend())
 
 
 
660
  } catch (e) {
661
  const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e); pre.style.color = 'var(--obl-cyan)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; host.appendChild(pre);
662
  }
app/src/content/embeds/d3-trackio.html CHANGED
@@ -125,7 +125,7 @@
125
  /* Global header (legend) above the grid and centered */
126
  .d3-trackio__header {
127
  display: flex;
128
- align-items: flex-start;
129
  justify-content: center;
130
  gap: 12px;
131
  margin: 0 0 10px 0;
@@ -142,7 +142,7 @@
142
  text-align: center;
143
  }
144
  .d3-trackio__header .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
145
- .d3-trackio__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
146
  .d3-trackio__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
147
  .d3-trackio__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; }
148
 
@@ -238,7 +238,7 @@
238
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
239
 
240
  // Layout & scales
241
- let width = 800, height = 200; const margin = { top: 10, right: 20, bottom: 46, left: 44 };
242
  const xScale = d3.scaleLinear();
243
  const yScale = d3.scaleLinear();
244
  const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
@@ -261,7 +261,9 @@
261
 
262
  // Axes
263
  gAxes.selectAll('*').remove();
264
- // Ticks: for x, force equal spacing on indices and include edges; for y, equally spaced within domain, count ~ x ticks bounded [2,6]
 
 
265
  const makeTicks = (scale, approx) => {
266
  const arr = scale.ticks(approx);
267
  const dom = scale.domain();
@@ -269,10 +271,33 @@
269
  if (arr[arr.length - 1] !== dom[dom.length - 1]) arr.push(dom[dom.length - 1]);
270
  return Array.from(new Set(arr));
271
  };
272
- const xTicksForced = (Array.isArray(xTicksArg) && xTicksArg.length)
273
- ? Array.from({ length: xTicksArg.length }, (_, i) => i)
274
- : makeTicks(xScale, 8);
275
- const yCount = Math.max(2, Math.min(6, xTicksForced.length));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  const yDom = yScale.domain();
277
  const yTicksForced = (yCount <= 2)
278
  ? [yDom[0], yDom[1]]
@@ -392,7 +417,7 @@
392
  // Points
393
  const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
394
  const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
395
- ptsSel.enter().append('circle').attr('class','pt').attr('data-run', d=>d.run).attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
396
  .attr('stroke', 'none').attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value))
397
  .merge(ptsSel)
398
  .each(function(d){ /* placeholder to keep merge chain intact */ })
@@ -433,11 +458,11 @@
433
  // No animation on chart while scaling up/down
434
  try {
435
  const sel = gPoints.selectAll('circle.pt');
436
- if (shouldSuppress()) { sel.interrupt().attr('r', d => (d && d.step === nearest ? 4 : 2)); }
437
- else { sel.transition().duration(140).ease(d3.easeCubicOut).attr('r', d => (d && d.step === nearest ? 4 : 2)); }
438
  } catch(_) {}
439
  }
440
- function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_){} } try { const sel = gPoints.selectAll('circle.pt'); if (shouldSuppress()) { sel.interrupt().attr('r', 2); } else { sel.transition().duration(150).ease(d3.easeCubicOut).attr('r', 2); } } catch(_) {} }, 100); }
441
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
442
  }
443
 
@@ -631,20 +656,46 @@
631
 
632
  // Build run list and shared color mapping
633
  const runList = Array.from(new Set(rows.map(r => String(r.run||'').trim()).filter(Boolean))).sort();
 
634
  let palette = null;
635
- try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') palette = window.ColorPalettes.getColors('categorical', runList.length); } catch(_) {}
636
  if (!palette) {
637
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
638
  palette = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
639
  }
640
- const colorForRun = (name) => palette[runList.indexOf(name) % palette.length];
641
 
642
  // Populate legend
643
  const legendItemsHost = legend.querySelector('.items');
644
- legendItemsHost.innerHTML = runList.map((name) => {
645
- const color = colorForRun(name);
646
- return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
647
- }).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
 
649
  // Build per-metric data map (using resolved names)
650
  const dataByMetric = new Map();
@@ -665,31 +716,119 @@
665
 
666
  // Resize handling
667
  const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
668
- host.__rerender = rerender;
669
- if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
670
-
671
- // Legend hover ghosting across all cells
672
- legendItemsHost.querySelectorAll('.item').forEach(el => {
673
- el.addEventListener('mouseenter', () => {
674
- const run = el.getAttribute('data-run'); if (!run) return;
675
- host.classList.add('hovering');
676
- host.querySelectorAll('.cell').forEach(cell => {
677
- cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
678
- cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
679
- cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));
 
 
 
 
 
680
  });
681
- legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
682
  });
683
- el.addEventListener('mouseleave', () => {
684
- host.classList.remove('hovering');
685
- host.querySelectorAll('.cell').forEach(cell => {
686
- cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));
687
- cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));
688
- cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  });
690
- legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
691
- });
692
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  } catch (e) {
694
  const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
695
  pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; host.appendChild(pre);
 
125
  /* Global header (legend) above the grid and centered */
126
  .d3-trackio__header {
127
  display: flex;
128
+ align-items: center;
129
  justify-content: center;
130
  gap: 12px;
131
  margin: 0 0 10px 0;
 
142
  text-align: center;
143
  }
144
  .d3-trackio__header .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
145
+ .d3-trackio__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; justify-content: center; align-items: center; }
146
  .d3-trackio__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
147
  .d3-trackio__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; }
148
 
 
238
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
239
 
240
  // Layout & scales
241
+ let width = 800, height = 10; const margin = { top: 10, right: 20, bottom: 46, left: 44 };
242
  const xScale = d3.scaleLinear();
243
  const yScale = d3.scaleLinear();
244
  const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
 
261
 
262
  // Axes
263
  gAxes.selectAll('*').remove();
264
+ // Ticks (Oblivion logic):
265
+ // - X: nice values mapped to nearest indices, include edges, density by width
266
+ // - Y: at least 5 ticks, density by height
267
  const makeTicks = (scale, approx) => {
268
  const arr = scale.ticks(approx);
269
  const dom = scale.domain();
 
271
  if (arr[arr.length - 1] !== dom[dom.length - 1]) arr.push(dom[dom.length - 1]);
272
  return Array.from(new Set(arr));
273
  };
274
+ const maxXTicks = Math.max(3, Math.min(12, Math.floor(innerWidth / 90)));
275
+ let xTicksForced = (Array.isArray(xTicksArg) && xTicksArg.length) ? [] : makeTicks(xScale, 8);
276
+ if (Array.isArray(xTicksArg) && xTicksArg.length) {
277
+ const realMin = xTicksArg[0];
278
+ const realMax = xTicksArg[xTicksArg.length - 1];
279
+ const niceVals = d3.ticks(realMin, realMax, maxXTicks);
280
+ const nearestIndex = (val) => {
281
+ let bestIdx = 0; let bestDist = Infinity;
282
+ for (let i = 0; i < xTicksArg.length; i++) {
283
+ const d = Math.abs(xTicksArg[i] - val);
284
+ if (d < bestDist) { bestDist = d; bestIdx = i; }
285
+ }
286
+ return bestIdx;
287
+ };
288
+ const indices = Array.from(new Set(niceVals.map(v => nearestIndex(v)))).sort((a,b)=>a-b);
289
+ if (indices[0] !== 0) indices.unshift(0);
290
+ const lastIdx = xTicksArg.length - 1;
291
+ if (indices[indices.length - 1] !== lastIdx) indices.push(lastIdx);
292
+ xTicksForced = indices;
293
+ } else if (xTicksForced.length > maxXTicks) {
294
+ const stride = Math.max(1, Math.ceil(xTicksForced.length / maxXTicks));
295
+ const last = xTicksForced[xTicksForced.length - 1];
296
+ xTicksForced = xTicksForced.filter((_, idx) => (idx % stride) === 0);
297
+ if (xTicksForced[xTicksForced.length - 1] !== last) xTicksForced.push(last);
298
+ }
299
+ const maxYTicks = Math.max(5, Math.min(6, Math.floor(innerHeight / 60)));
300
+ const yCount = maxYTicks;
301
  const yDom = yScale.domain();
302
  const yTicksForced = (yCount <= 2)
303
  ? [yDom[0], yDom[1]]
 
417
  // Points
418
  const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
419
  const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
420
+ ptsSel.enter().append('circle').attr('class','pt').attr('data-run', d=>d.run).attr('r', 0).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
421
  .attr('stroke', 'none').attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value))
422
  .merge(ptsSel)
423
  .each(function(d){ /* placeholder to keep merge chain intact */ })
 
458
  // No animation on chart while scaling up/down
459
  try {
460
  const sel = gPoints.selectAll('circle.pt');
461
+ if (shouldSuppress()) { sel.interrupt().attr('r', d => (d && d.step === nearest ? 4 : 0)); }
462
+ else { sel.transition().duration(140).ease(d3.easeCubicOut).attr('r', d => (d && d.step === nearest ? 4 : 0)); }
463
  } catch(_) {}
464
  }
465
+ function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); if (host) { try { host.dispatchEvent(new CustomEvent('trackio-hover-clear')); } catch(_){} } try { const sel = gPoints.selectAll('circle.pt'); if (shouldSuppress()) { sel.interrupt().attr('r', 0); } else { sel.transition().duration(150).ease(d3.easeCubicOut).attr('r', 0); } } catch(_) {} }, 100); }
466
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
467
  }
468
 
 
656
 
657
  // Build run list and shared color mapping
658
  const runList = Array.from(new Set(rows.map(r => String(r.run||'').trim()).filter(Boolean))).sort();
659
+ let currentRunList = runList.slice();
660
  let palette = null;
661
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') palette = window.ColorPalettes.getColors('categorical', currentRunList.length); } catch(_) {}
662
  if (!palette) {
663
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
664
  palette = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
665
  }
666
+ const colorForRun = (name) => palette[currentRunList.indexOf(name) % palette.length];
667
 
668
  // Populate legend
669
  const legendItemsHost = legend.querySelector('.items');
670
+ function rebuildLegend(){
671
+ legendItemsHost.innerHTML = currentRunList.map((name) => {
672
+ const color = colorForRun(name);
673
+ return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
674
+ }).join('');
675
+ // Bind hover ghosting
676
+ legendItemsHost.querySelectorAll('.item').forEach(el => {
677
+ el.addEventListener('mouseenter', () => {
678
+ const run = el.getAttribute('data-run'); if (!run) return;
679
+ host.classList.add('hovering');
680
+ host.querySelectorAll('.cell').forEach(cell => {
681
+ cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
682
+ cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
683
+ cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));
684
+ });
685
+ legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
686
+ });
687
+ el.addEventListener('mouseleave', () => {
688
+ host.classList.remove('hovering');
689
+ host.querySelectorAll('.cell').forEach(cell => {
690
+ cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));
691
+ cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));
692
+ cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));
693
+ });
694
+ legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
695
+ });
696
+ });
697
+ }
698
+ rebuildLegend();
699
 
700
  // Build per-metric data map (using resolved names)
701
  const dataByMetric = new Map();
 
716
 
717
  // Resize handling
718
  const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
719
+ // Synthetic data support: jitter (r) and simulation (s)
720
+ let cycleIdx = 2;
721
+ function jitterData(){
722
+ const mList = (metricsToDraw && metricsToDraw.length) ? metricsToDraw : TARGET_METRICS;
723
+ mList.forEach((tgt) => {
724
+ if (tgt === 'epoch') return;
725
+ const map = dataByMetric.get(tgt); if (!map) return;
726
+ const isAcc = /acc/i.test(tgt); const isLoss = /loss/i.test(tgt);
727
+ const scale = isAcc ? 0.01 : (isLoss ? 0.03 : 0.02);
728
+ Object.keys(map).forEach((run) => {
729
+ map[run].forEach((pt) => {
730
+ const base = Math.abs(pt.value) || 1;
731
+ const delta = (Math.random()*2-1) * scale * base;
732
+ let nv = pt.value + delta;
733
+ if (isAcc) nv = Math.max(0, Math.min(1, nv));
734
+ pt.value = nv;
735
+ });
736
  });
 
737
  });
738
+ }
739
+ function genCurves(n){
740
+ const quality = Math.random(); const good = quality > 0.66; const poor = quality < 0.33;
741
+ const l0 = 2.0 + Math.random()*4.5;
742
+ const targetLoss = good ? l0*(0.12+Math.random()*0.12) : (poor ? l0*(0.35+Math.random()*0.25) : l0*(0.22+Math.random()*0.16));
743
+ const phases = 1 + Math.floor(Math.random()*3);
744
+ const marksSet = new Set();
745
+ while (marksSet.size < phases-1) { marksSet.add(Math.floor((0.25+Math.random()*0.5)*(n-1))); }
746
+ const marks = [0, ...Array.from(marksSet).sort((a,b)=>a-b), n-1];
747
+ let kLoss = 0.02 + Math.random()*0.08; const loss = new Array(n);
748
+ for (let seg=0; seg<marks.length-1; seg++){
749
+ const a=marks[seg], b=marks[seg+1]||a+1;
750
+ for (let i=a;i<=b;i++){
751
+ const t = (i-a)/Math.max(1,(b-a));
752
+ const segTarget = targetLoss * Math.pow(0.85, seg);
753
+ let v = l0*Math.exp(-kLoss*(i+1));
754
+ v = 0.6*v + 0.4*(l0 + (segTarget - l0)*(seg + t)/Math.max(1,(marks.length-1)));
755
+ const noiseAmp = (0.08*l0) * (1 - 0.8*(i/(n-1)));
756
+ v += (Math.random()*2-1)*noiseAmp; if (Math.random() < 0.02) v += 0.15*l0;
757
+ loss[i] = Math.max(0, v);
758
+ }
759
+ kLoss *= 1.6;
760
+ }
761
+ const a0 = 0.1 + Math.random()*0.35; const aMax = good ? (0.92+Math.random()*0.07) : (poor ? (0.62+Math.random()*0.14) : (0.8+Math.random()*0.1));
762
+ let kAcc = 0.02 + Math.random()*0.08; const acc = new Array(n);
763
+ for (let i=0;i<n;i++){
764
+ let v = aMax - (aMax - a0)*Math.exp(-kAcc*(i+1));
765
+ const noiseAmp = 0.04*(1 - 0.8*(i/(n-1)));
766
+ v += (Math.random()*2-1)*noiseAmp; acc[i] = Math.max(0, Math.min(1, v));
767
+ if (marksSet.has(i)) kAcc *= 1.4;
768
+ }
769
+ const accGap = 0.02 + Math.random()*0.06; const lossGap = 0.05 + Math.random()*0.15;
770
+ const accVal = new Array(n); const lossVal = new Array(n);
771
+ let ofStart = Math.floor(((good ? 0.85 : 0.7) + (Math.random()*0.15 - 0.05))*(n-1));
772
+ ofStart = Math.max(Math.floor(0.5*(n-1)), Math.min(Math.floor(0.95*(n-1)), ofStart));
773
+ for (let i=0;i<n;i++){
774
+ let av = acc[i] - accGap + (Math.random()*0.06 - 0.03);
775
+ let lv = loss[i]*(1+lossGap) + (Math.random()*0.1 - 0.05)*Math.max(1, l0*0.2);
776
+ if (i>=ofStart && !poor){ const t = (i-ofStart)/Math.max(1,(n-1-ofStart)); av -= 0.03*t; lv += 0.12*t*loss[i]; }
777
+ accVal[i] = Math.max(0, Math.min(1, av)); lossVal[i] = Math.max(0, lv);
778
+ }
779
+ return { accTrain: acc, lossTrain: loss, accVal, lossVal };
780
+ }
781
+ function generateRunNames(count){
782
+ const adjectives = ['ancient','brave','calm','clever','crimson','daring','eager','fearless','gentle','glossy','golden','hidden','icy','jolly','lively','mighty','noble','proud','quick','silent','swift','tiny','vivid','wild'];
783
+ const nouns = ['river','mountain','harbor','forest','valley','ocean','meadow','desert','island','canyon','harbor','trail','summit','delta','lagoon','ridge','tundra','reef','plateau','prairie','grove','bay','dune','cliff'];
784
+ const used = new Set(); const names = [];
785
+ const pick = (arr) => arr[Math.floor(Math.random()*arr.length)];
786
+ while (names.length < count) {
787
+ const name = `${pick(adjectives)}-${pick(nouns)}-${Math.floor(1+Math.random()*9)}`;
788
+ if (!used.has(name)) { used.add(name); names.push(name); }
789
+ }
790
+ return names;
791
+ }
792
+ function simulateData(){
793
+ // Randomize number of runs between 2 and 7 with W&B-like names
794
+ const wantRuns = Math.max(2, Math.floor(2 + Math.random()*6));
795
+ const runsSim = generateRunNames(wantRuns);
796
+ const rnd = (min,max)=> Math.floor(min + Math.random()*(max-min+1));
797
+ let stepsCount = 16;
798
+ if (cycleIdx === 0) stepsCount = rnd(4, 12);
799
+ else if (cycleIdx === 1) stepsCount = rnd(16, 48);
800
+ else stepsCount = rnd(80, 240);
801
+ cycleIdx = (cycleIdx + 1) % 3;
802
+ const steps = Array.from({length: stepsCount}, (_,i)=> i+1);
803
+ const nextByMetric = new Map();
804
+ const mList = (metricsToDraw && metricsToDraw.length) ? metricsToDraw : TARGET_METRICS;
805
+ mList.forEach(tgt => { const map = {}; runsSim.forEach(r => map[r] = []); nextByMetric.set(tgt, map); });
806
+ runsSim.forEach(run => {
807
+ const curves = genCurves(stepsCount);
808
+ steps.forEach((s,i)=>{
809
+ if (mList.includes('epoch')) nextByMetric.get('epoch')[run].push({ step:s, value:s });
810
+ if (mList.includes('train_accuracy')) nextByMetric.get('train_accuracy')[run].push({ step:s, value: curves.accTrain[i] });
811
+ if (mList.includes('val_accuracy')) nextByMetric.get('val_accuracy')[run].push({ step:s, value: curves.accVal[i] });
812
+ if (mList.includes('train_loss')) nextByMetric.get('train_loss')[run].push({ step:s, value: curves.lossTrain[i] });
813
+ if (mList.includes('val_loss')) nextByMetric.get('val_loss')[run].push({ step:s, value: curves.lossVal[i] });
814
  });
815
+ });s
816
+ nextByMetric.forEach((v,k)=> dataByMetric.set(k,v));
817
+ // Update currentRunList and palette, then legend
818
+ currentRunList = runsSim.slice();
819
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') palette = window.ColorPalettes.getColors('categorical', currentRunList.length); } catch(_) {}
820
+ if (!palette) { const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; palette = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])]; }
821
+ rebuildLegend();
822
+ }
823
+ const onKeyDownSim = (ev) => { try { const key = ev && ev.key ? ev.key.toLowerCase() : ''; if (key==='r') { jitterData(); rerender(); } if (key==='s') { simulateData(); rerender(); } } catch(_) {} };
824
+ window.addEventListener('keydown', onKeyDownSim);
825
+ // Start with level 3 (long) synthetic data by default
826
+ simulateData();
827
+ rerender();
828
+ host.__rerender = rerender;
829
+ if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
830
+
831
+ // (Legend hover handlers bound inside rebuildLegend())
832
  } catch (e) {
833
  const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
834
  pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; host.appendChild(pre);
app/src/styles/components/_form.css ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Form Elements - Modern Minimal Design */
3
+ /* ============================================================================ */
4
+
5
+ /* Select styling with modern chevron */
6
+ select {
7
+ background-color: var(--page-bg);
8
+ border: 1px solid var(--border-color);
9
+ border-radius: var(--button-radius);
10
+ padding: var(--button-padding-y) var(--button-padding-x) var(--button-padding-y) var(--button-padding-x);
11
+ font-family: var(--default-font-family);
12
+ font-size: var(--button-font-size);
13
+ color: var(--text-color);
14
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");
15
+ background-repeat: no-repeat;
16
+ background-position: right calc(var(--button-padding-x) + 14px) center;
17
+ background-size: 12px;
18
+ cursor: pointer;
19
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
20
+ }
21
+
22
+ select:hover, select:focus, select:active {
23
+ border-color: var(--primary-color);
24
+ }
25
+
26
+ select:focus {
27
+ outline: none;
28
+ border-color: var(--primary-color);
29
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
30
+ }
31
+
32
+ select:disabled {
33
+ opacity: 0.6;
34
+ cursor: not-allowed;
35
+ background-color: var(--surface-bg);
36
+ }
37
+
38
+ /* Dark theme select chevron */
39
+ [data-theme="dark"] select {
40
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23bbb' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");
41
+ }
42
+
43
+ /* Checkbox styling */
44
+ input[type="checkbox"] {
45
+ appearance: none;
46
+ width: 16px;
47
+ height: 16px;
48
+ border: 2px solid var(--border-color);
49
+ border-radius: 3px;
50
+ background-color: var(--page-bg);
51
+ cursor: pointer;
52
+ position: relative;
53
+ transition: all 0.2s ease;
54
+ margin-right: var(--spacing-2);
55
+ }
56
+
57
+ input[type="checkbox"]:hover {
58
+ border-color: var(--primary-color);
59
+ }
60
+
61
+ input[type="checkbox"]:focus {
62
+ outline: none;
63
+ border-color: var(--primary-color);
64
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
65
+ }
66
+
67
+ input[type="checkbox"]:checked {
68
+ background-color: var(--primary-color);
69
+ border-color: var(--primary-color);
70
+ }
71
+
72
+ input[type="checkbox"]:checked::before {
73
+ content: '';
74
+ position: absolute;
75
+ top: 1px;
76
+ left: 4px;
77
+ width: 4px;
78
+ height: 8px;
79
+ border: solid var(--on-primary);
80
+ border-width: 0 2px 2px 0;
81
+ transform: rotate(45deg);
82
+ }
83
+
84
+ input[type="checkbox"]:disabled {
85
+ opacity: 0.6;
86
+ cursor: not-allowed;
87
+ }
88
+
89
+ /* Radio button styling */
90
+ input[type="radio"] {
91
+ appearance: none;
92
+ width: 16px;
93
+ height: 16px;
94
+ border: 2px solid var(--border-color);
95
+ border-radius: 50%;
96
+ background-color: var(--page-bg);
97
+ cursor: pointer;
98
+ position: relative;
99
+ transition: all 0.2s ease;
100
+ margin-right: var(--spacing-2);
101
+ }
102
+
103
+ input[type="radio"]:hover {
104
+ border-color: var(--primary-color);
105
+ }
106
+
107
+ input[type="radio"]:focus {
108
+ outline: none;
109
+ border-color: var(--primary-color);
110
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
111
+ }
112
+
113
+ input[type="radio"]:checked {
114
+ border-color: var(--primary-color);
115
+ }
116
+
117
+ input[type="radio"]:checked::before {
118
+ content: '';
119
+ position: absolute;
120
+ top: 2px;
121
+ left: 2px;
122
+ width: 8px;
123
+ height: 8px;
124
+ border-radius: 50%;
125
+ background-color: var(--primary-color);
126
+ }
127
+
128
+ input[type="radio"]:disabled {
129
+ opacity: 0.6;
130
+ cursor: not-allowed;
131
+ }
132
+
133
+ /* Input text styling for consistency */
134
+ input[type="text"],
135
+ input[type="email"],
136
+ input[type="password"],
137
+ input[type="number"],
138
+ input[type="url"],
139
+ input[type="search"],
140
+ textarea {
141
+ appearance: none;
142
+ background-color: var(--page-bg);
143
+ border: 1px solid var(--border-color);
144
+ border-radius: var(--button-radius);
145
+ padding: var(--button-padding-y) var(--button-padding-x);
146
+ font-family: var(--default-font-family);
147
+ font-size: var(--button-font-size);
148
+ color: var(--text-color);
149
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
150
+ width: 100%;
151
+ }
152
+
153
+ input[type="text"]:hover,
154
+ input[type="email"]:hover,
155
+ input[type="password"]:hover,
156
+ input[type="number"]:hover,
157
+ input[type="url"]:hover,
158
+ input[type="search"]:hover,
159
+ textarea:hover {
160
+ border-color: var(--primary-color);
161
+ }
162
+
163
+ input[type="text"]:focus,
164
+ input[type="email"]:focus,
165
+ input[type="password"]:focus,
166
+ input[type="number"]:focus,
167
+ input[type="url"]:focus,
168
+ input[type="search"]:focus,
169
+ textarea:focus {
170
+ outline: none;
171
+ border-color: var(--primary-color);
172
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
173
+ }
174
+
175
+ input[type="text"]:disabled,
176
+ input[type="email"]:disabled,
177
+ input[type="password"]:disabled,
178
+ input[type="number"]:disabled,
179
+ input[type="url"]:disabled,
180
+ input[type="search"]:disabled,
181
+ textarea:disabled {
182
+ opacity: 0.6;
183
+ cursor: not-allowed;
184
+ background-color: var(--surface-bg);
185
+ }
186
+
187
+ /* Label styling */
188
+ label {
189
+ display: flex;
190
+ align-items: center;
191
+ font-size: var(--button-font-size);
192
+ color: var(--text-color);
193
+ cursor: pointer;
194
+ margin-bottom: 0;
195
+ line-height: 1.4;
196
+ user-select: none;
197
+ }
198
+
199
+ /* Form group spacing */
200
+ .form-group {
201
+ margin-bottom: var(--spacing-4);
202
+ display: flex;
203
+ align-items: center;
204
+ gap: var(--spacing-2);
205
+ }
206
+
207
+ .form-group label {
208
+ margin-bottom: 0;
209
+ }
210
+
211
+ /* Alternative: for vertical form groups */
212
+ .form-group.vertical {
213
+ flex-direction: column;
214
+ align-items: flex-start;
215
+ }
216
+
217
+ .form-group.vertical label {
218
+ margin-bottom: var(--spacing-1);
219
+ }
220
+
221
+ /* For inline form elements */
222
+ .form-inline {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: var(--spacing-2);
226
+ margin-bottom: var(--spacing-3);
227
+ }
228
+
229
+ .form-inline label {
230
+ margin-bottom: 0;
231
+ }
232
+
233
+ /* Ensure labels in flex containers don't break alignment */
234
+ div[style*="display: flex"] label,
235
+ div[class*="flex"] label,
236
+ .trackio-controls label,
237
+ .scale-controls label,
238
+ .theme-selector label {
239
+ margin-bottom: 0 !important;
240
+ align-self: center;
241
+ }
app/src/styles/global.css CHANGED
@@ -8,6 +8,7 @@
8
  @import './components/_table.css';
9
  @import './components/_tag.css';
10
  @import './components/_card.css';
 
11
 
12
  .demo-wide,
13
  .demo-full-width {
 
8
  @import './components/_table.css';
9
  @import './components/_tag.css';
10
  @import './components/_card.css';
11
+ @import './components/_form.css';
12
 
13
  .demo-wide,
14
  .demo-full-width {