thibaud frere commited on
Commit
4bcb726
·
1 Parent(s): 0c262e2

update trackio redesign experiment

Browse files
Files changed (32) hide show
  1. app/.astro/astro/content.d.ts +5 -0
  2. app/src/components/Hero.astro +2 -2
  3. app/src/components/TrackioWrapper.astro +5 -2
  4. app/src/components/trackio/Cell.svelte +174 -697
  5. app/src/components/trackio/ChartRenderer.svelte +500 -0
  6. app/src/components/trackio/ChartTooltip.svelte +245 -0
  7. app/src/components/trackio/FullscreenModal.svelte +618 -0
  8. app/src/components/trackio/Legend.svelte +62 -6
  9. app/src/components/trackio/Tooltip.svelte +1 -1
  10. app/src/components/trackio/Trackio.svelte +118 -2
  11. app/src/components/trackio/themes/_components.css +0 -561
  12. app/src/components/trackio/themes/_theme-base.css +0 -49
  13. app/src/components/trackio/themes/_theme-classic.css +0 -69
  14. app/src/components/trackio/themes/_theme-neon.css +0 -73
  15. app/src/components/trackio/themes/_theme-oblivion.css +0 -106
  16. app/src/components/trackio/themes/index.css +0 -41
  17. app/src/content/assets/data/comparison/id_1_query.png +0 -3
  18. app/src/content/assets/data/comparison/id_1_rank_1_sim_1.000.png +0 -3
  19. app/src/content/assets/data/comparison/id_1_rank_2_sim_0.165.png +0 -3
  20. app/src/content/assets/data/comparison/id_1_rank_3_sim_0.143.png +0 -3
  21. app/src/content/assets/data/comparison/id_2_rank_1_sim_1.000.png +0 -3
  22. app/src/content/assets/data/comparison/id_2_rank_2_sim_0.978.png +0 -3
  23. app/src/content/assets/data/comparison/id_2_rank_3_sim_0.975.png +0 -3
  24. app/src/content/assets/data/comparison/id_3_query.png +0 -3
  25. app/src/content/assets/data/comparison/id_3_rank_1_sim_0.936.png +0 -3
  26. app/src/content/assets/data/comparison/id_3_rank_2_sim_0.686.png +0 -3
  27. app/src/content/assets/data/comparison/id_3_rank_3_sim_0.676.png +0 -3
  28. app/src/content/assets/data/{comparison/id_2_query.png → llm_benchmarks.json} +2 -2
  29. app/src/content/chapters/vibe-coding-charts.mdx +1 -2
  30. app/src/content/embeds/d3-trackio-oblivion.html +0 -670
  31. app/src/content/embeds/d3-trackio.html +0 -843
  32. app/src/styles/components/_button.css +8 -8
app/.astro/astro/content.d.ts CHANGED
@@ -230,6 +230,11 @@ declare module 'astro:content' {
230
 
231
  type DataEntryMap = {
232
  "assets": {
 
 
 
 
 
233
  "data/mnist-variant-model": {
234
  id: "data/mnist-variant-model";
235
  collection: "assets";
 
230
 
231
  type DataEntryMap = {
232
  "assets": {
233
+ "data/llm_benchmarks": {
234
+ id: "data/llm_benchmarks";
235
+ collection: "assets";
236
+ data: any
237
+ };
238
  "data/mnist-variant-model": {
239
  id: "data/mnist-variant-model";
240
  collection: "assets";
app/src/components/Hero.astro CHANGED
@@ -128,9 +128,9 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
128
  <div class="meta-container-cell meta-container-cell--pdf">
129
  <h3>PDF</h3>
130
  <p>
131
- <a class="button" href={`/${pdfFilename}`} download={pdfFilename} aria-label={`Download PDF ${pdfFilename}`}>
132
  Download PDF
133
- </a>
134
  </p>
135
  </div>
136
  </div>
 
128
  <div class="meta-container-cell meta-container-cell--pdf">
129
  <h3>PDF</h3>
130
  <p>
131
+ <button class="button" href={`/${pdfFilename}`} download={pdfFilename} aria-label={`Download PDF ${pdfFilename}`}>
132
  Download PDF
133
+ </button>
134
  </p>
135
  </div>
136
  </div>
app/src/components/TrackioWrapper.astro CHANGED
@@ -164,7 +164,7 @@ import Trackio from './trackio/Trackio.svelte';
164
  align-items: center;
165
  margin-bottom: 16px;
166
  padding: 12px 0px;
167
- border-bottom: 1px solid var(--border-color);
168
  gap: 16px;
169
  flex-wrap: nowrap;
170
  }
@@ -259,7 +259,10 @@ import Trackio from './trackio/Trackio.svelte';
259
 
260
  .trackio-container {
261
  width: 100%;
262
- margin-top: 40px;
 
 
 
263
  }
264
 
265
  @media (max-width: 768px) {
 
164
  align-items: center;
165
  margin-bottom: 16px;
166
  padding: 12px 0px;
167
+ /* border-bottom: 1px solid var(--border-color); */
168
  gap: 16px;
169
  flex-wrap: nowrap;
170
  }
 
259
 
260
  .trackio-container {
261
  width: 100%;
262
+ margin-top: 10px;
263
+ border: 1px solid var(--border-color);
264
+ padding: 24px 12px;
265
+
266
  }
267
 
268
  @media (max-width: 768px) {
app/src/components/trackio/Cell.svelte CHANGED
@@ -1,8 +1,9 @@
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;
@@ -12,554 +13,171 @@
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
@@ -652,44 +270,8 @@
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;
@@ -714,15 +296,18 @@
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;
@@ -730,7 +315,7 @@
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 */
@@ -742,17 +327,24 @@
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) {
@@ -774,129 +366,14 @@
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
-
 
1
  <script>
2
+ import ChartRenderer from './ChartRenderer.svelte';
3
+ import ChartTooltip from './ChartTooltip.svelte';
4
+ import { formatAbbrev } from './chart-utils.js';
5
+
6
+ // Props
7
  export let metricKey;
8
  export let titleText;
9
  export let wide = false;
 
13
  export let smoothing = false;
14
  export let metricData = {}; // { run -> [{step,value}] } - smoothed data
15
  export let rawMetricData = {}; // { run -> [{step,value}] } - original data for background when smoothing
16
+ export let colorForRun = (name) => '#999';
17
  export let hostEl = null;
18
 
19
+ // Navigation props
20
+ export let currentIndex = 0;
21
+ export let onOpenModal = null;
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ // Component state
24
+ let root;
25
+ let chartRenderer; // Reference to ChartRenderer component
26
+
27
+ // Tooltip state
28
+ let tooltipVisible = false;
29
+ let tooltipX = -9999;
30
+ let tooltipY = -9999;
31
+ let tooltipTitle = '';
32
+ let tooltipSubtitle = '';
33
+ let tooltipEntries = [];
34
+
35
+ // Handlers
36
+ function openFullscreen() {
37
+ if (onOpenModal) {
38
+ onOpenModal(currentIndex);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
+
42
+ function handleChartHover(data) {
43
+ console.log('🎯 Cell.svelte handleChartHover called with:', data);
44
+ const { step, entries, position } = data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ if (entries.length) {
47
+ // Use global mouse coordinates for tooltip positioning
48
+ const trackioEl = hostEl.closest('.trackio');
49
+ const trackioRect = trackioEl.getBoundingClientRect();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ // Position tooltip near global cursor with small offset
52
+ const relativeX = (position.globalX || position.x) - trackioRect.left + 15;
53
+ const relativeY = (position.globalY || position.y) - trackioRect.top + 15;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ tooltipVisible = true;
56
+ tooltipX = Math.round(relativeX);
57
+ tooltipY = Math.round(relativeY);
58
+ tooltipTitle = `Step ${formatAbbrev(step)}`;
59
+ tooltipSubtitle = titleText;
60
+ tooltipEntries = entries;
61
 
62
+ console.log('📍 Tooltip state updated:', { tooltipVisible, tooltipX, tooltipY, tooltipTitle, entriesCount: tooltipEntries.length });
63
+
64
+ // Dispatch to host for cross-cell synchronization
65
+ try {
66
+ hostEl && hostEl.dispatchEvent(new CustomEvent('trackio-hover-step', {
67
+ detail: { step, sourceMetric: metricKey }
68
+ }));
69
+ } catch(_) {}
70
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
+
73
+ function handleChartLeave() {
74
+ tooltipVisible = false;
75
+ tooltipX = -9999;
76
+ tooltipY = -9999;
77
+
78
+ // Dispatch leave event
79
+ try {
80
+ hostEl && hostEl.dispatchEvent(new CustomEvent('trackio-hover-clear', {
81
+ detail: { sourceMetric: metricKey }
82
+ }));
83
+ } catch(_) {}
 
 
 
 
 
 
 
84
  }
85
+
86
+ // External hover synchronization
87
+ function setupExternalHover() {
88
+ if (!root || root.__syncAttached || !hostEl) return;
89
 
90
+ hostEl.addEventListener('trackio-hover-step', (ev) => {
91
+ const d = ev && ev.detail;
92
+ if (!d || !chartRenderer) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ // Don't sync to self - avoid infinite loops
95
+ if (d.sourceMetric === metricKey) return;
 
 
96
 
97
+ // Show hover line at the specified step
98
+ chartRenderer.showHoverLine(d.step);
99
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ hostEl.addEventListener('trackio-hover-clear', (ev) => {
102
+ if (!chartRenderer) return;
103
+
104
+ // Don't sync to self
105
+ const d = ev && ev.detail;
106
+ if (d && d.sourceMetric === metricKey) return;
107
 
108
+ // Hide hover line
109
+ chartRenderer.hideHoverLine();
 
 
 
 
 
 
 
 
 
110
  });
111
+
112
+ root.__syncAttached = true;
113
+ }
114
+
115
+ $: if (root && hostEl) {
116
+ setupExternalHover();
117
  }
118
  </script>
119
 
120
+ <div
121
+ class="cell {wide ? 'cell--wide' : ''}"
122
+ bind:this={root}
123
+ data-metric={metricKey}
124
+ data-title={titleText}
125
+ data-variant={variant}
126
+ >
127
+ <div class="cell-bg"></div>
128
+ <div class="cell-corners"></div>
129
+ <div class="cell-inner">
130
+ <div class="cell-header">
131
+ <div class="cell-title">
132
+ {titleText}
133
+ </div>
134
+ <button
135
+ class="cell-fullscreen-btn"
136
+ type="button"
137
+ on:click={openFullscreen}
138
+ title="Fullscreen"
139
+ >
140
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
141
+ <path d="M4 9V4h5v2H6v3H4zm10-5h5v5h-2V6h-3V4zM6 18h3v2H4v-5h2v3zm12-3h2v5h-5v-2h3v-3z"/>
142
+ </svg>
143
+ </button>
144
+ </div>
145
+
146
+ <div class="cell-body">
147
+ <ChartRenderer
148
+ bind:this={chartRenderer}
149
+ {metricData}
150
+ {rawMetricData}
151
+ {colorForRun}
152
+ {variant}
153
+ {logScaleX}
154
+ {smoothing}
155
+ {normalizeLoss}
156
+ {metricKey}
157
+ {titleText}
158
+ {hostEl}
159
+ width={800}
160
+ height={150}
161
+ onHover={handleChartHover}
162
+ onLeave={handleChartLeave}
163
+ />
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <!-- Tooltip -->
169
+ <ChartTooltip
170
+ visible={tooltipVisible}
171
+ x={tooltipX}
172
+ y={tooltipY}
173
+ title={tooltipTitle}
174
+ subtitle={tooltipSubtitle}
175
+ entries={tooltipEntries}
176
+ parentElement={root}
177
+ />
178
+
179
+
180
+
181
  <style>
182
  /* =========================
183
  CELL BASE STYLES
 
270
  linear-gradient(#ffffff, #ffffff) bottom right / 1px 8px no-repeat;
271
  }
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  :global(.trackio .cell-header) {
274
+ padding: 0 0px 10px 10px;
275
  display: flex;
276
  align-items: center;
277
  justify-content: space-between;
 
296
  overflow: hidden;
297
  }
298
 
299
+ /* Oblivion theme overrides */
300
+ :global(.trackio.theme--oblivion .cell) {
301
+ border: none !important;
302
+ background: transparent !important;
303
+ }
304
+
305
+ :global(.trackio.theme--classic .cell) {
306
+ border: 1px solid var(--trackio-cell-border) !important;
307
+ background: var(--trackio-cell-background) !important;
308
+ border-radius: 10px !important;
309
  }
 
 
310
 
 
311
  :global(.trackio.theme--oblivion .cell-title) {
312
  font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
313
  letter-spacing: 0.12em !important;
 
315
  font-weight: 800 !important;
316
  font-size: 12px !important;
317
  position: relative;
318
+ padding-left: 14px;
319
  }
320
 
321
  /* Oblivion theme: add indicator dot before title */
 
327
  transform: translateY(-50%);
328
  width: 6px;
329
  height: 6px;
330
+ background: var(--trackio-chart-axis-text);
331
+ border: 1px solid var(--trackio-chart-axis-stroke);
332
+ border-radius: 1px;
333
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.1) inset;
334
+ opacity: 0.6;
335
  }
336
 
337
+
338
  /* Ghost hover effect */
339
  :global(.trackio.hovering .ghost) {
340
  opacity: 0.2;
341
  transition: opacity 0.15s ease;
342
  }
343
+
344
+ /* Specific ghost effect for raw lines when smoothing is active */
345
+ :global(.trackio.hovering path.raw-line.ghost) {
346
+ opacity: 0.1;
347
+ }
348
 
349
  /* Wide cell spans full width */
350
  :global(.trackio__grid .cell--wide) {
 
366
  border-radius: 6px;
367
  transition: opacity 0.15s ease;
368
  }
369
+
370
  .cell-fullscreen-btn:hover {
371
  opacity: 1;
 
372
  }
373
+
374
  .cell-fullscreen-btn svg {
375
  width: 18px;
376
  height: 18px;
377
  fill: var(--trackio-chart-axis-text);
378
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/components/trackio/ChartRenderer.svelte ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import * as d3 from 'd3';
3
+ import { onMount, onDestroy } from 'svelte';
4
+ import { formatAbbrev, formatLogTick, generateSmartTicks, generateLogTicks } from './chart-utils.js';
5
+
6
+ // Props
7
+ export let metricData = {};
8
+ export let rawMetricData = {};
9
+ export let colorForRun = (name) => '#999';
10
+ export let variant = 'classic';
11
+ export let logScaleX = false;
12
+ export let smoothing = false;
13
+ export let normalizeLoss = true;
14
+ export let metricKey = '';
15
+ export let titleText = '';
16
+ export let hostEl = null;
17
+ export let width = 800;
18
+ export let height = 150;
19
+ export let margin = { top: 10, right: 12, bottom: 46, left: 44 };
20
+ export let onHover = null; // Callback for hover events
21
+ export let onLeave = null; // Callback for leave events
22
+
23
+ // SVG elements
24
+ let container;
25
+ let svg, gRoot, gGrid, gGridDots, gAxes, gAreas, gLines, gPoints, gHover;
26
+ let xScale, yScale, lineGen;
27
+ let cleanup;
28
+ let hoverLine; // Reference to hover line for external control
29
+
30
+ $: innerHeight = height - margin.top - margin.bottom;
31
+
32
+ // Reactive re-render when data or any relevant prop changes
33
+ $: {
34
+ if (container) {
35
+ // List all dependencies to trigger render when any change
36
+ void metricData;
37
+ void metricKey;
38
+ void variant;
39
+ void logScaleX;
40
+ void normalizeLoss;
41
+ void smoothing;
42
+ render();
43
+ }
44
+ }
45
+
46
+ function ensureSvg() {
47
+ if (svg || !container) return;
48
+
49
+ const d3Container = d3.select(container);
50
+ svg = d3Container.append('svg')
51
+ .attr('width', '100%')
52
+ .style('display', 'block');
53
+
54
+ gRoot = svg.append('g');
55
+ gGrid = gRoot.append('g').attr('class', 'grid');
56
+ gGridDots = gRoot.append('g').attr('class', 'grid-dots');
57
+ gAxes = gRoot.append('g').attr('class', 'axes');
58
+ gAreas = gRoot.append('g').attr('class', 'areas');
59
+ gLines = gRoot.append('g').attr('class', 'lines');
60
+ gPoints = gRoot.append('g').attr('class', 'points');
61
+ gHover = gRoot.append('g').attr('class', 'hover');
62
+
63
+ // Initialize scales
64
+ xScale = logScaleX ? d3.scaleLog() : d3.scaleLinear();
65
+ yScale = d3.scaleLinear();
66
+ lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
67
+ }
68
+
69
+ function updateLayout(hoverSteps) {
70
+ if (!svg || !container) return { innerWidth: 0, innerHeight: 0, xTicksForced: [], yTicksForced: [] };
71
+
72
+ const fontFamily = 'var(--trackio-font-family)';
73
+
74
+ // Calculate actual container width
75
+ const rect = container.getBoundingClientRect();
76
+ const actualWidth = Math.max(1, Math.round(rect && rect.width ? rect.width : (container.clientWidth || width)));
77
+
78
+ svg.attr('width', actualWidth)
79
+ .attr('height', height)
80
+ .attr('viewBox', `0 0 ${actualWidth} ${height}`)
81
+ .attr('preserveAspectRatio', 'xMidYMid meet');
82
+
83
+ const innerWidth = actualWidth - margin.left - margin.right;
84
+
85
+ gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
86
+ xScale.range([0, innerWidth]);
87
+ yScale.range([innerHeight, 0]);
88
+
89
+ gAxes.selectAll('*').remove();
90
+
91
+ const minXTicks = 5;
92
+ const maxXTicks = Math.max(minXTicks, Math.min(12, Math.floor(innerWidth / 70)));
93
+ let xTicksForced = [];
94
+
95
+ let logTickData = null;
96
+ if (logScaleX) {
97
+ logTickData = generateLogTicks(hoverSteps, minXTicks, maxXTicks, innerWidth, xScale);
98
+ xTicksForced = logTickData.major;
99
+ } else if (Array.isArray(hoverSteps) && hoverSteps.length) {
100
+ const tickIndices = generateSmartTicks(hoverSteps, minXTicks, maxXTicks, innerWidth);
101
+ xTicksForced = tickIndices;
102
+ } else {
103
+ const makeTicks = (scale, approx) => {
104
+ const arr = scale.ticks(approx);
105
+ const dom = scale.domain();
106
+ if (arr.length === 0 || arr[0] !== dom[0]) arr.unshift(dom[0]);
107
+ if (arr[arr.length-1] !== dom[dom.length-1]) arr.push(dom[dom.length-1]);
108
+ return Array.from(new Set(arr));
109
+ };
110
+ xTicksForced = makeTicks(xScale, maxXTicks);
111
+ }
112
+
113
+ const maxYTicks = Math.max(5, Math.min(6, Math.floor(innerHeight / 60)));
114
+ const yCount = maxYTicks;
115
+ const yDom = yScale.domain();
116
+ const yTicksForced = (yCount <= 2) ? [yDom[0], yDom[1]] : Array.from({length:yCount}, (_,i)=> yDom[0] + ((yDom[1]-yDom[0])*(i/(yCount-1))));
117
+
118
+ // Draw axes
119
+ gAxes.append('g')
120
+ .attr('transform', `translate(0,${innerHeight})`)
121
+ .call(d3.axisBottom(xScale).tickValues(xTicksForced).tickFormat((val) => {
122
+ const displayVal = logScaleX ? val : (Array.isArray(hoverSteps) && hoverSteps[val] != null ? hoverSteps[val] : val);
123
+ return logScaleX ? formatLogTick(displayVal, true) : formatAbbrev(displayVal);
124
+ }))
125
+ .call(g => {
126
+ g.selectAll('path, line').style('stroke', 'var(--trackio-chart-axis-stroke)');
127
+ g.selectAll('text').style('fill', 'var(--trackio-chart-axis-text)').style('font-size','11px').style('font-family', fontFamily)
128
+ .style('font-weight', d => {
129
+ if (!logScaleX) return 'normal';
130
+ const log10 = Math.log10(Math.abs(d));
131
+ const isPowerOf10 = Math.abs(log10 % 1) < 0.01;
132
+ return isPowerOf10 ? '600' : 'normal';
133
+ });
134
+ });
135
+
136
+ gAxes.append('g')
137
+ .call(d3.axisLeft(yScale).tickValues(yTicksForced).tickFormat((v) => formatAbbrev(v)))
138
+ .call(g => {
139
+ g.selectAll('path, line').style('stroke', 'var(--trackio-chart-axis-stroke)');
140
+ g.selectAll('text').style('fill', 'var(--trackio-chart-axis-text)').style('font-size','11px').style('font-family', fontFamily);
141
+ });
142
+
143
+ // Add minor ticks for logarithmic scale
144
+ if (logScaleX && logTickData && logTickData.minor.length > 0) {
145
+ gAxes.append('g').attr('class', 'minor-ticks').attr('transform', `translate(0,${innerHeight})`)
146
+ .selectAll('line.minor-tick')
147
+ .data(logTickData.minor)
148
+ .join('line')
149
+ .attr('class', 'minor-tick')
150
+ .attr('x1', d => xScale(d))
151
+ .attr('x2', d => xScale(d))
152
+ .attr('y1', 0)
153
+ .attr('y2', 4)
154
+ .style('stroke', 'var(--trackio-chart-axis-stroke)')
155
+ .style('stroke-opacity', 0.4)
156
+ .style('stroke-width', 0.5);
157
+ }
158
+
159
+ // Steps label
160
+ const labelY = innerHeight + Math.max(20, Math.min(36, margin.bottom - 12));
161
+ const stepsText = logScaleX ? 'Steps (log)' : 'Steps';
162
+
163
+ gAxes.append('text')
164
+ .attr('class','x-axis-label')
165
+ .attr('x', innerWidth/2)
166
+ .attr('y', labelY)
167
+ .style('fill', 'var(--trackio-chart-axis-text)')
168
+ .attr('text-anchor','middle')
169
+ .style('font-size','9px')
170
+ .style('opacity','.9')
171
+ .style('letter-spacing','.5px')
172
+ .style('text-transform','uppercase')
173
+ .style('font-weight','500')
174
+ .style('font-family', fontFamily)
175
+ .text(stepsText);
176
+
177
+ return { innerWidth, innerHeight, xTicksForced, yTicksForced };
178
+ }
179
+
180
+ function renderGrid(xTicksForced, yTicksForced, hoverSteps) {
181
+ const shouldUseDots = variant === 'oblivion';
182
+
183
+ if (shouldUseDots) {
184
+ // Oblivion-style: Grid as dots
185
+ gGrid.selectAll('*').remove();
186
+ gGridDots.selectAll('*').remove();
187
+ const gridPoints = [];
188
+ const yMin = yScale.domain()[0];
189
+ const desiredCols = 24;
190
+ const xGridStride = Math.max(1, Math.ceil(hoverSteps.length/desiredCols));
191
+ const xGridIdx = [];
192
+ for (let idx = 0; idx < hoverSteps.length; idx += xGridStride) xGridIdx.push(idx);
193
+ if (xGridIdx[xGridIdx.length-1] !== hoverSteps.length-1) xGridIdx.push(hoverSteps.length-1);
194
+ xGridIdx.forEach(i => {
195
+ yTicksForced.forEach(t => {
196
+ if (i !== 0 && (yMin == null || t !== yMin)) gridPoints.push({ sx: i, ty: t });
197
+ });
198
+ });
199
+ gGridDots.selectAll('circle.grid-dot')
200
+ .data(gridPoints)
201
+ .join('circle')
202
+ .attr('class', 'grid-dot')
203
+ .attr('cx', d => xScale(d.sx))
204
+ .attr('cy', d => yScale(d.ty))
205
+ .attr('r', 1.25)
206
+ .style('fill', 'var(--trackio-chart-grid-stroke)')
207
+ .style('fill-opacity', 'var(--trackio-chart-grid-opacity)');
208
+ } else {
209
+ // Classic-style: Grid as lines
210
+ gGridDots.selectAll('*').remove();
211
+ gGrid.selectAll('*').remove();
212
+
213
+ // Horizontal grid lines
214
+ const xRange = xScale.range();
215
+ const maxX = Math.max(...xRange);
216
+
217
+ gGrid.selectAll('line.horizontal')
218
+ .data(yTicksForced)
219
+ .join('line')
220
+ .attr('class', 'horizontal')
221
+ .attr('x1', 0).attr('x2', maxX)
222
+ .attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
223
+ .style('stroke', 'var(--trackio-chart-grid-stroke)')
224
+ .style('stroke-opacity', 'var(--trackio-chart-grid-opacity)')
225
+ .attr('stroke-width', 1);
226
+
227
+ // Vertical grid lines
228
+ gGrid.selectAll('line.vertical')
229
+ .data(xTicksForced)
230
+ .join('line')
231
+ .attr('class', 'vertical')
232
+ .attr('x1', d => xScale(d)).attr('x2', d => xScale(d))
233
+ .attr('y1', 0).attr('y2', innerHeight)
234
+ .style('stroke', 'var(--trackio-chart-grid-stroke)')
235
+ .style('stroke-opacity', 'var(--trackio-chart-grid-opacity)')
236
+ .attr('stroke-width', 1);
237
+ }
238
+ }
239
+
240
+ function render() {
241
+ ensureSvg();
242
+ if (!svg || !gRoot) return;
243
+
244
+ const runs = Object.keys(metricData || {});
245
+ const hasAny = runs.some(r => (metricData[r] || []).length > 0);
246
+ if (!hasAny) {
247
+ gRoot.style('display', 'none');
248
+ return;
249
+ }
250
+ gRoot.style('display', null);
251
+
252
+ // Data processing
253
+ let minStep = Infinity, maxStep = -Infinity, minVal = Infinity, maxVal = -Infinity;
254
+ runs.forEach(r => {
255
+ (metricData[r] || []).forEach(pt => {
256
+ minStep = Math.min(minStep, pt.step);
257
+ maxStep = Math.max(maxStep, pt.step);
258
+ minVal = Math.min(minVal, pt.value);
259
+ maxVal = Math.max(maxVal, pt.value);
260
+ });
261
+ });
262
+
263
+ const isAccuracy = /accuracy/i.test(metricKey);
264
+ const isLoss = /loss/i.test(metricKey);
265
+ if (isAccuracy) yScale.domain([0,1]).nice();
266
+ else if (isLoss && normalizeLoss) yScale.domain([0,1]).nice();
267
+ else yScale.domain([minVal, maxVal]).nice();
268
+
269
+ const stepSet = new Set();
270
+ runs.forEach(r => (metricData[r] || []).forEach(v => stepSet.add(v.step)));
271
+ const hoverSteps = Array.from(stepSet).sort((a,b) => a - b);
272
+
273
+ // Update X scale based on logScaleX prop
274
+ xScale = logScaleX ? d3.scaleLog() : d3.scaleLinear();
275
+
276
+ let stepIndex = null;
277
+
278
+ if (logScaleX) {
279
+ const minStep = Math.max(1, Math.min(...hoverSteps));
280
+ const maxStep = Math.max(...hoverSteps);
281
+ xScale.domain([minStep, maxStep]);
282
+ lineGen.x(d => xScale(d.step));
283
+ } else {
284
+ stepIndex = new Map(hoverSteps.map((s,i) => [s,i]));
285
+ xScale.domain([0, Math.max(0, hoverSteps.length - 1)]);
286
+ lineGen.x(d => xScale(stepIndex.get(d.step)));
287
+ }
288
+
289
+ const normalizeY = (v) => (isLoss && normalizeLoss ? ((maxVal > minVal) ? (v - minVal) / (maxVal - minVal) : 0) : v);
290
+ lineGen.y(d => yScale(normalizeY(d.value)));
291
+
292
+ const { xTicksForced, yTicksForced } = updateLayout(hoverSteps);
293
+
294
+ // Render grid
295
+ renderGrid(xTicksForced, yTicksForced, hoverSteps);
296
+
297
+ // Render data
298
+ const series = runs.map(r => ({
299
+ run: r,
300
+ color: colorForRun(r),
301
+ values: (metricData[r] || []).slice().sort((a,b) => a.step - b.step)
302
+ }));
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 => ({
307
+ run: r,
308
+ color: colorForRun(r),
309
+ values: (rawMetricData[r] || []).slice().sort((a,b) => a.step - b.step)
310
+ }));
311
+ const rawPaths = gLines.selectAll('path.raw-line').data(rawSeries, d => d.run + '-raw');
312
+ rawPaths.enter().append('path').attr('class','raw-line').attr('data-run', d => d.run).attr('fill','none').attr('stroke-width',1).attr('opacity',0.2).attr('stroke', d => d.color).style('pointer-events','none').attr('d', d => lineGen(d.values));
313
+ rawPaths.attr('stroke', d => d.color).attr('opacity',0.2).attr('d', d => lineGen(d.values));
314
+ rawPaths.exit().remove();
315
+ } else {
316
+ gLines.selectAll('path.raw-line').remove();
317
+ }
318
+
319
+ // Draw main lines
320
+ const paths = gLines.selectAll('path.run-line').data(series, d => d.run);
321
+ 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));
322
+ paths.transition().duration(160).attr('stroke', d => d.color).attr('opacity',0.9).attr('d', d => lineGen(d.values));
323
+ paths.exit().remove();
324
+
325
+ // Draw points
326
+ const allPoints = series.flatMap(s => s.values.map(v => ({ run: s.run, color: s.color, step: v.step, value: v.value })));
327
+ const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d => `${d.run}-${d.step}`);
328
+ 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')
329
+ .attr('cx', d => logScaleX ? xScale(d.step) : xScale(stepIndex.get(d.step)))
330
+ .attr('cy', d => yScale(normalizeY(d.value)))
331
+ .merge(ptsSel)
332
+ .attr('cx', d => logScaleX ? xScale(d.step) : xScale(stepIndex.get(d.step)))
333
+ .attr('cy', d => yScale(normalizeY(d.value)));
334
+ ptsSel.exit().remove();
335
+
336
+ // Setup hover interactions
337
+ setupHoverInteractions(hoverSteps, stepIndex, series, normalizeY, isAccuracy, innerWidth);
338
+ }
339
+
340
+ function setupHoverInteractions(hoverSteps, stepIndex, series, normalizeY, isAccuracy, innerWidth) {
341
+ if (!gHover || !container) return;
342
+
343
+ gHover.selectAll('*').remove();
344
+
345
+ // Recalculate dimensions to be sure
346
+ const actualWidth = container.getBoundingClientRect().width;
347
+ const currentInnerWidth = innerWidth || (actualWidth - margin.left - margin.right);
348
+ const currentInnerHeight = innerHeight || (height - margin.top - margin.bottom);
349
+
350
+ const overlay = gHover.append('rect')
351
+ .attr('fill','transparent')
352
+ .style('cursor','crosshair')
353
+ .attr('x',0)
354
+ .attr('y',0)
355
+ .attr('width', currentInnerWidth)
356
+ .attr('height', currentInnerHeight)
357
+ .style('pointer-events','all');
358
+
359
+ hoverLine = gHover.append('line')
360
+ .style('stroke','var(--text-color)')
361
+ .attr('stroke-opacity',0.25)
362
+ .attr('stroke-width',1)
363
+ .attr('y1',0)
364
+ .attr('y2',innerHeight)
365
+ .style('display','none')
366
+ .style('pointer-events','none');
367
+
368
+ let hideTipTimer = null;
369
+
370
+ function onMove(ev) {
371
+ console.log('onMove called, checking series...');
372
+ try {
373
+ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; }
374
+ const [mx, my] = d3.pointer(ev, overlay.node());
375
+ console.log('Got mouse position:', mx, my);
376
+
377
+ // Get global mouse coordinates for tooltip positioning
378
+ const globalX = ev.clientX;
379
+ const globalY = ev.clientY;
380
+
381
+ let nearest, xpx;
382
+ if (logScaleX) {
383
+ const mouseStepValue = xScale.invert(mx);
384
+ let minDist = Infinity;
385
+ let closestStep = hoverSteps[0];
386
+ hoverSteps.forEach(step => {
387
+ const dist = Math.abs(Math.log(step) - Math.log(mouseStepValue));
388
+ if (dist < minDist) {
389
+ minDist = dist;
390
+ closestStep = step;
391
+ }
392
+ });
393
+ nearest = closestStep;
394
+ xpx = xScale(nearest);
395
+ } else {
396
+ const idx = Math.round(Math.max(0, Math.min(hoverSteps.length-1, xScale.invert(mx))));
397
+ nearest = hoverSteps[idx];
398
+ xpx = xScale(idx);
399
+ }
400
+
401
+ hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
402
+
403
+ console.log('About to use series, available?', typeof series);
404
+ const entries = series.map(s => {
405
+ const m = new Map(s.values.map(v => [v.step, v]));
406
+ const pt = m.get(nearest);
407
+ return { run: s.run, color: s.color, pt };
408
+ }).filter(e => e.pt && e.pt.value != null).sort((a,b) => a.pt.value - b.pt.value);
409
+
410
+ const fmt = (vv) => (isAccuracy ? (+vv).toFixed(4) : (+vv).toFixed(4));
411
+
412
+ // Call parent hover callback
413
+ console.log('About to call onHover:', { hasOnHover: !!onHover, entriesLength: entries.length, step: nearest });
414
+ if (onHover) {
415
+ onHover({
416
+ step: nearest,
417
+ entries: entries.map(e => ({
418
+ color: e.color,
419
+ name: e.run,
420
+ valueText: fmt(e.pt.value)
421
+ })),
422
+ position: { x: mx, y: my, globalX, globalY }
423
+ });
424
+ console.log('onHover callback completed');
425
+ } else {
426
+ console.log('onHover is null!');
427
+ }
428
+
429
+ try {
430
+ gPoints.selectAll('circle.pt').attr('r', d => (d && d.step === nearest ? 4 : 0));
431
+ } catch(_) {}
432
+ } catch(error) {
433
+ console.error('Error in onMove:', error);
434
+ }
435
+ }
436
+
437
+ function onMouseLeave() {
438
+ hideTipTimer = setTimeout(() => {
439
+ hoverLine.style('display','none');
440
+ if (onLeave) onLeave();
441
+ try {
442
+ gPoints.selectAll('circle.pt').attr('r', 0);
443
+ } catch(_) {}
444
+ }, 0);
445
+ }
446
+
447
+ overlay.on('mousemove', function() {
448
+ console.log('OVERLAY MOUSEMOVE DETECTED!');
449
+ onMove.apply(this, arguments);
450
+ }).on('mouseleave', onMouseLeave);
451
+ }
452
+
453
+ onMount(() => {
454
+ render();
455
+ const ro = window.ResizeObserver ? new ResizeObserver(() => render()) : null;
456
+ if (ro && container) ro.observe(container);
457
+ cleanup = () => { ro && ro.disconnect(); };
458
+ });
459
+
460
+ onDestroy(() => {
461
+ cleanup && cleanup();
462
+ });
463
+
464
+ // Public methods for external hover control
465
+ export function showHoverLine(step) {
466
+ if (!hoverLine || !xScale || !container) return;
467
+
468
+ try {
469
+ let xpx;
470
+ if (logScaleX) {
471
+ xpx = xScale(step);
472
+ } else {
473
+ // Find step index in hoverSteps
474
+ const stepSet = new Set();
475
+ Object.keys(metricData || {}).forEach(r =>
476
+ (metricData[r] || []).forEach(v => stepSet.add(v.step))
477
+ );
478
+ const hoverSteps = Array.from(stepSet).sort((a,b) => a - b);
479
+ const stepIndex = hoverSteps.indexOf(step);
480
+ if (stepIndex >= 0) {
481
+ xpx = xScale(stepIndex);
482
+ }
483
+ }
484
+
485
+ if (xpx !== undefined) {
486
+ hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
487
+ }
488
+ } catch (e) {
489
+ console.warn('Error showing hover line:', e);
490
+ }
491
+ }
492
+
493
+ export function hideHoverLine() {
494
+ if (hoverLine) {
495
+ hoverLine.style('display', 'none');
496
+ }
497
+ }
498
+ </script>
499
+
500
+ <div bind:this={container} style="width: 100%; height: 100%;"></div>
app/src/components/trackio/ChartTooltip.svelte ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { onMount } from 'svelte';
3
+
4
+ // Props
5
+ export let visible = false;
6
+ export let x = -9999;
7
+ export let y = -9999;
8
+ export let title = '';
9
+ export let subtitle = '';
10
+ export let entries = []; // Array of { color, name, valueText }
11
+ export let parentElement = null; // Element to append tooltip to
12
+ export let zIndex = 1000; // Default z-index
13
+
14
+ let tooltipElement;
15
+ let tipHost;
16
+
17
+ onMount(() => {
18
+ // Find trackio parent for positioning context
19
+ console.log('ChartTooltip onMount - parentElement:', parentElement);
20
+ console.log('ChartTooltip onMount - parentElement classes:', parentElement?.className);
21
+
22
+ const trackioEl = parentElement?.closest?.('.trackio') || (parentElement?.classList?.contains('trackio') ? parentElement : null);
23
+
24
+ console.log('ChartTooltip onMount - trackioEl found:', trackioEl);
25
+
26
+ if (trackioEl) {
27
+ // Create tooltip host positioned relative to trackio container
28
+ tipHost = document.createElement('div');
29
+ tipHost.className = 'tip-host';
30
+ tipHost.style.position = 'absolute';
31
+ tipHost.style.top = '0';
32
+ tipHost.style.left = '0';
33
+ tipHost.style.width = '100%';
34
+ tipHost.style.height = '100%';
35
+ tipHost.style.pointerEvents = 'none';
36
+ tipHost.style.zIndex = String(zIndex);
37
+ tipHost.style.overflow = 'visible';
38
+ trackioEl.appendChild(tipHost);
39
+
40
+ // Move tooltip element to host
41
+ if (tooltipElement && tipHost) {
42
+ tipHost.appendChild(tooltipElement);
43
+ }
44
+ }
45
+
46
+ return () => {
47
+ if (tipHost && tipHost.parentNode) {
48
+ tipHost.parentNode.removeChild(tipHost);
49
+ }
50
+ };
51
+ });
52
+
53
+ $: tooltipStyle = `
54
+ transform: translate(${x}px, ${y}px);
55
+ pointer-events: none;
56
+ z-index: ${zIndex};
57
+ `;
58
+
59
+ $: tooltipClass = `d3-tooltip ${visible ? 'is-visible' : ''}`;
60
+ </script>
61
+
62
+ <div
63
+ bind:this={tooltipElement}
64
+ class={tooltipClass}
65
+ style={tooltipStyle}
66
+ >
67
+ <div class="d3-tooltip__inner">
68
+ {#if title}
69
+ <div class="d3-tooltip__title">{@html title}</div>
70
+ {/if}
71
+
72
+ {#if subtitle}
73
+ <div class="d3-tooltip__subtitle">{subtitle}</div>
74
+ {/if}
75
+
76
+ {#each entries as entry}
77
+ <div class="d3-tooltip__entry">
78
+ <span class="d3-tooltip__color-dot" style="background:{entry.color}"></span>
79
+ <strong class="d3-tooltip__entry-name">{entry.name}</strong>
80
+ <span class="d3-tooltip__entry-value">{entry.valueText}</span>
81
+ </div>
82
+ {/each}
83
+ </div>
84
+ </div>
85
+
86
+ <style>
87
+ /* Classic tooltip styling - based on original d3-trackio.html */
88
+ .d3-tooltip {
89
+ position: absolute;
90
+ top: 0;
91
+ left: 0;
92
+ transform: translate(-9999px, -9999px);
93
+ pointer-events: none;
94
+ padding: 10px 12px;
95
+ border-radius: 12px;
96
+ font-size: 12px;
97
+ line-height: 1.35;
98
+ border: 1px solid var(--border-color);
99
+ background: var(--surface-bg);
100
+ color: var(--text-color);
101
+ box-shadow: 0 8px 32px rgba(0,0,0,.12), 0 2px 8px rgba(0,0,0,.06);
102
+ opacity: 0;
103
+ transition: none;
104
+ z-index: var(--z-tooltip, 50);
105
+ backdrop-filter: saturate(1.12) blur(8px);
106
+ }
107
+
108
+ .d3-tooltip.is-visible {
109
+ opacity: 1;
110
+ }
111
+
112
+ .d3-tooltip__inner {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 6px;
116
+ min-width: 220px;
117
+ }
118
+
119
+ .d3-tooltip__title {
120
+ font-weight: 800;
121
+ letter-spacing: 0.1px;
122
+ margin-bottom: 0;
123
+ color: var(--text-color);
124
+ }
125
+
126
+ .d3-tooltip__subtitle {
127
+ font-size: 11px;
128
+ color: var(--muted-color);
129
+ display: block;
130
+ margin-top: -4px;
131
+ margin-bottom: 2px;
132
+ letter-spacing: 0.1px;
133
+ }
134
+
135
+ .d3-tooltip__entry {
136
+ padding-top: 6px;
137
+ border-top: 1px solid var(--border-color);
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 8px;
141
+ white-space: nowrap;
142
+ }
143
+
144
+ .d3-tooltip__entry-name {
145
+ flex: 1;
146
+ font-weight: 500;
147
+ }
148
+
149
+ .d3-tooltip__entry-value {
150
+ margin-left: auto;
151
+ text-align: right;
152
+ font-weight: 900;
153
+ }
154
+
155
+ .d3-tooltip__color-dot {
156
+ display: inline-block;
157
+ width: 12px;
158
+ height: 12px;
159
+ border-radius: 3px;
160
+ border: 1px solid var(--border-color);
161
+ flex-shrink: 0;
162
+ }
163
+
164
+ /* Oblivion tooltip styling - based on original d3-trackio-oblivion.html */
165
+ :global(.trackio.theme--oblivion) .d3-tooltip {
166
+ border-radius: 8px;
167
+ border: none;
168
+ background:
169
+ radial-gradient(1200px 200px at 20% -10%, rgba(0,0,0,.05), transparent 80%),
170
+ radial-gradient(900px 200px at 80% 110%, rgba(0,0,0,.05), transparent 80%);
171
+ color: var(--trackio-text-primary);
172
+ box-shadow: 0 8px 32px rgba(127,241,255,.05), 0 2px 8px rgba(0,0,0,.10);
173
+ opacity: 0;
174
+ backdrop-filter: saturate(1.1) blur(10px);
175
+ }
176
+
177
+ :global(.trackio.theme--oblivion) .d3-tooltip.is-visible {
178
+ opacity: 1;
179
+ }
180
+
181
+ /* Dark mode oblivion tooltip */
182
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion) .d3-tooltip {
183
+ background:
184
+ radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, #ffffff 6.5%, transparent), transparent 80%),
185
+ radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, #ffffff 6%, transparent), transparent 80%),
186
+ linear-gradient(180deg, color-mix(in srgb, #ffffff 3.5%, transparent), transparent 45%);
187
+ color: var(--trackio-text-primary);
188
+ box-shadow: 0 8px 32px color-mix(in srgb, #ffffff 5%, transparent), 0 2px 8px color-mix(in srgb, black 10%, transparent);
189
+ }
190
+
191
+ :global(.trackio.theme--oblivion) .d3-tooltip__title {
192
+ font-weight: 900;
193
+ letter-spacing: 0.18em;
194
+ text-transform: uppercase;
195
+ color: var(--trackio-oblivion-primary);
196
+ }
197
+
198
+ :global(.trackio.theme--oblivion) .d3-tooltip__subtitle {
199
+ color: var(--trackio-oblivion-primary);
200
+ opacity: 0.4;
201
+ letter-spacing: 0.06em;
202
+ }
203
+
204
+ :global(.trackio.theme--oblivion) .d3-tooltip__entry {
205
+ position: relative;
206
+ padding-top: 10px;
207
+ margin-top: 6px;
208
+ border-top: none;
209
+ }
210
+
211
+ :global(.trackio.theme--oblivion) .d3-tooltip__entry::before {
212
+ content: "";
213
+ position: absolute;
214
+ left: 0;
215
+ right: 0;
216
+ top: 0;
217
+ height: 1px;
218
+ background: color-mix(in srgb, #000000 10%, transparent);
219
+ }
220
+
221
+ :global(.trackio.theme--oblivion) .d3-tooltip__entry::after {
222
+ content: "";
223
+ position: absolute;
224
+ left: 0;
225
+ right: 0;
226
+ top: 1px;
227
+ height: 1px;
228
+ background: color-mix(in srgb, #ffffff 15%, transparent);
229
+ }
230
+
231
+ /* Dark mode separator adjustments */
232
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion) .d3-tooltip__entry::before {
233
+ background: color-mix(in srgb, #ffffff 5%, transparent);
234
+ }
235
+
236
+ :global([data-theme="dark"]) :global(.trackio.theme--oblivion) .d3-tooltip__entry::after {
237
+ background: color-mix(in srgb, #000000 10%, transparent);
238
+ }
239
+
240
+ :global(.trackio.theme--oblivion) .d3-tooltip__color-dot {
241
+ border-radius: 2px;
242
+ border: 1px solid var(--obl-border, rgba(50, 50, 50, 0.22));
243
+ box-shadow: 0 0 10px color-mix(in srgb, var(--obl-base, #323232) 20%, transparent) inset;
244
+ }
245
+ </style>
app/src/components/trackio/FullscreenModal.svelte ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { createEventDispatcher } from 'svelte';
3
+ import ChartRenderer from './ChartRenderer.svelte';
4
+ import ChartTooltip from './ChartTooltip.svelte';
5
+ import Legend from './Legend.svelte';
6
+ import { formatAbbrev } from './chart-utils.js';
7
+
8
+ // Props
9
+ export let visible = false;
10
+ export let title = '';
11
+ export let metricData = {};
12
+ export let rawMetricData = {};
13
+ export let colorForRun = (name) => '#999';
14
+ export let variant = 'classic';
15
+ export let logScaleX = false;
16
+ export let smoothing = false;
17
+ export let normalizeLoss = true;
18
+ export let metricKey = '';
19
+ export let titleText = '';
20
+
21
+ // Navigation props
22
+ export let currentIndex = 0;
23
+ export let totalCharts = 1;
24
+ export let onNavigate = null;
25
+
26
+ const dispatch = createEventDispatcher();
27
+
28
+ let modalElement;
29
+
30
+ // Tooltip state (same as Cell.svelte)
31
+ let tooltipVisible = false;
32
+ let tooltipX = -9999;
33
+ let tooltipY = -9999;
34
+ let tooltipTitle = '';
35
+ let tooltipSubtitle = '';
36
+ let tooltipEntries = [];
37
+
38
+ // Modal management
39
+ $: if (visible && modalElement) {
40
+ document.body.appendChild(modalElement);
41
+
42
+ // Copy CSS variables from the trackio parent to ensure theme inheritance
43
+ const trackioParent = document.querySelector('.trackio');
44
+ if (trackioParent) {
45
+ const computedStyle = getComputedStyle(trackioParent);
46
+ const cssVars = [
47
+ '--trackio-chart-axis-stroke',
48
+ '--trackio-chart-axis-text',
49
+ '--trackio-chart-grid-stroke',
50
+ '--trackio-chart-grid-opacity',
51
+ '--trackio-chart-grid-type',
52
+ '--trackio-font-family',
53
+ '--trackio-tooltip-background',
54
+ '--trackio-tooltip-border',
55
+ '--trackio-tooltip-shadow',
56
+ '--trackio-text-primary',
57
+ '--trackio-text-secondary'
58
+ ];
59
+
60
+ cssVars.forEach(varName => {
61
+ const value = computedStyle.getPropertyValue(varName);
62
+ if (value) {
63
+ modalElement.style.setProperty(varName, value);
64
+ }
65
+ });
66
+ }
67
+
68
+ requestAnimationFrame(() => {
69
+ modalElement.classList.add('show');
70
+ });
71
+ }
72
+
73
+ function closeModal() {
74
+ if (modalElement) {
75
+ modalElement.classList.remove('show');
76
+ setTimeout(() => {
77
+ if (modalElement && modalElement.parentNode) {
78
+ modalElement.parentNode.removeChild(modalElement);
79
+ }
80
+ dispatch('close');
81
+ }, 300);
82
+ }
83
+ }
84
+
85
+ function handleKeydown(e) {
86
+ if (e.key === 'Escape') {
87
+ closeModal();
88
+ } else if (e.key === 'ArrowLeft') {
89
+ navigatePrevious();
90
+ } else if (e.key === 'ArrowRight') {
91
+ navigateNext();
92
+ }
93
+ }
94
+
95
+ function navigatePrevious() {
96
+ if (onNavigate && totalCharts > 1) {
97
+ const newIndex = currentIndex === 0 ? totalCharts - 1 : currentIndex - 1;
98
+ onNavigate(newIndex);
99
+ }
100
+ }
101
+
102
+ function navigateNext() {
103
+ if (onNavigate && totalCharts > 1) {
104
+ const newIndex = currentIndex === totalCharts - 1 ? 0 : currentIndex + 1;
105
+ onNavigate(newIndex);
106
+ }
107
+ }
108
+
109
+ function handleOverlayClick(e) {
110
+ if (e.target === e.currentTarget) {
111
+ closeModal();
112
+ }
113
+ }
114
+
115
+ // Prepare legend data
116
+ $: runs = Object.keys(metricData);
117
+ $: legendData = runs.map(run => ({
118
+ name: run,
119
+ color: colorForRun(run)
120
+ }));
121
+
122
+ // Tooltip handling (same logic as Cell.svelte)
123
+ function handleChartHover(data) {
124
+ const { step, entries, position } = data;
125
+
126
+ if (entries.length) {
127
+ // Use global mouse coordinates for tooltip positioning
128
+ const modalRect = modalElement.getBoundingClientRect();
129
+
130
+ // Position tooltip near global cursor with small offset
131
+ const relativeX = (position.globalX || position.x) - modalRect.left + 15;
132
+ const relativeY = (position.globalY || position.y) - modalRect.top + 15;
133
+
134
+ tooltipVisible = true;
135
+ tooltipX = Math.round(relativeX);
136
+ tooltipY = Math.round(relativeY);
137
+ tooltipTitle = `Step ${formatAbbrev(step)}`;
138
+ tooltipSubtitle = titleText || metricKey;
139
+ tooltipEntries = entries;
140
+ }
141
+ }
142
+
143
+ function handleChartLeave() {
144
+ tooltipVisible = false;
145
+ tooltipX = -9999;
146
+ tooltipY = -9999;
147
+ }
148
+
149
+ // Ghost legend functionality
150
+ function handleLegendHover(idx) {
151
+ legendData.forEach((otherItem, otherIdx) => {
152
+ if (otherIdx !== idx) {
153
+ const legendItems = modalElement?.querySelectorAll('.item');
154
+ if (legendItems && legendItems[otherIdx]) {
155
+ legendItems[otherIdx].classList.add('ghost');
156
+ }
157
+
158
+ const chartElements = modalElement?.querySelectorAll(`[data-run="${otherItem.name}"]`);
159
+ chartElements?.forEach(el => el.classList.add('ghost'));
160
+ }
161
+ });
162
+
163
+ // Add hovering class to trigger the ghost styles
164
+ const modalChart = modalElement?.querySelector('.trackio-modal-chart-content');
165
+ modalChart?.classList.add('hovering');
166
+ }
167
+
168
+ function handleLegendLeave() {
169
+ const legendItems = modalElement?.querySelectorAll('.item');
170
+ legendItems?.forEach(item => item.classList.remove('ghost'));
171
+
172
+ const chartElements = modalElement?.querySelectorAll('[data-run]');
173
+ chartElements?.forEach(el => el.classList.remove('ghost'));
174
+
175
+ // Remove hovering class
176
+ const modalChart = modalElement?.querySelector('.trackio-modal-chart-content');
177
+ modalChart?.classList.remove('hovering');
178
+ }
179
+ </script>
180
+
181
+ <!-- Modal overlay -->
182
+ {#if visible}
183
+ <div
184
+ bind:this={modalElement}
185
+ class="trackio-modal-overlay trackio {variant === 'oblivion' ? 'theme--oblivion' : 'theme--classic'}"
186
+ on:click={handleOverlayClick}
187
+ on:keydown={handleKeydown}
188
+ role="dialog"
189
+ aria-modal="true"
190
+ tabindex="-1"
191
+ >
192
+ <div class="trackio-modal">
193
+ <!-- Header with improved layout -->
194
+ <div class="trackio-modal-header">
195
+ <div class="trackio-modal-header-left">
196
+ <h3>{title}</h3>
197
+ </div>
198
+
199
+ <div class="trackio-modal-header-right">
200
+ <!-- Navigation controls grouped with counter -->
201
+ <div class="trackio-modal-nav-counter-group">
202
+ {#if totalCharts > 1}
203
+ <button
204
+ class="trackio-modal-nav-inline trackio-modal-nav-inline-left"
205
+ on:click={navigatePrevious}
206
+ title="Previous chart (←)"
207
+ aria-label="Previous chart"
208
+ >
209
+ <svg viewBox="0 0 24 24" fill="currentColor">
210
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
211
+ </svg>
212
+ </button>
213
+ {/if}
214
+
215
+ <div class="trackio-modal-counter">
216
+ {currentIndex + 1}/{totalCharts}
217
+ </div>
218
+
219
+ {#if totalCharts > 1}
220
+ <button
221
+ class="trackio-modal-nav-inline trackio-modal-nav-inline-right"
222
+ on:click={navigateNext}
223
+ title="Next chart (→)"
224
+ aria-label="Next chart"
225
+ >
226
+ <svg viewBox="0 0 24 24" fill="currentColor">
227
+ <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
228
+ </svg>
229
+ </button>
230
+ {/if}
231
+ </div>
232
+
233
+ <button
234
+ class="trackio-modal-close"
235
+ on:click={closeModal}
236
+ title="Close"
237
+ aria-label="Close modal"
238
+ >
239
+ ×
240
+ </button>
241
+ </div>
242
+ </div>
243
+
244
+ <!-- Content -->
245
+ <div class="trackio-modal-content">
246
+ <!-- Legend -->
247
+ {#if legendData.length > 0}
248
+ <div class="trackio-modal-legend">
249
+ <Legend
250
+ items={legendData}
251
+ alignment="left"
252
+ on:legend-hover={(e) => handleLegendHover(legendData.findIndex(item => item.name === e.detail.name))}
253
+ on:legend-leave={handleLegendLeave}
254
+ />
255
+ </div>
256
+ {/if}
257
+
258
+ <!-- Chart -->
259
+ <div class="trackio-modal-chart-content trackio {variant === 'oblivion' ? 'theme--oblivion' : 'theme--classic'}">
260
+ <ChartRenderer
261
+ {metricData}
262
+ {rawMetricData}
263
+ {colorForRun}
264
+ {variant}
265
+ {logScaleX}
266
+ {smoothing}
267
+ {normalizeLoss}
268
+ {metricKey}
269
+ {titleText}
270
+ height={500}
271
+ margin={{ top: 20, right: 30, bottom: 46, left: 44 }}
272
+ onHover={handleChartHover}
273
+ onLeave={handleChartLeave}
274
+ />
275
+ </div>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- Tooltip (same as Cell.svelte but with higher z-index) -->
280
+ <ChartTooltip
281
+ visible={tooltipVisible}
282
+ x={tooltipX}
283
+ y={tooltipY}
284
+ title={tooltipTitle}
285
+ subtitle={tooltipSubtitle}
286
+ entries={tooltipEntries}
287
+ parentElement={modalElement}
288
+ zIndex={1000001}
289
+ />
290
+ </div>
291
+ {/if}
292
+
293
+ <style>
294
+ /* Modal styles */
295
+ :global(.trackio-modal-overlay) {
296
+ position: fixed;
297
+ inset: 0;
298
+ background: rgba(0, 0, 0, 0.8);
299
+ z-index: 999999;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ opacity: 0;
304
+ pointer-events: none;
305
+ transition: opacity 0.3s ease;
306
+ }
307
+
308
+ /* Light mode overlay */
309
+ :global([data-theme="light"]) :global(.trackio-modal-overlay) {
310
+ background: rgba(255, 255, 255, 0.85);
311
+ }
312
+
313
+ /* Dark mode overlay */
314
+ :global([data-theme="dark"]) :global(.trackio-modal-overlay) {
315
+ background: rgba(0, 0, 0, 0.8);
316
+ }
317
+
318
+ /* Oblivion theme overlay - light mode */
319
+ :global([data-theme="light"]) :global(.trackio-modal-overlay.theme--oblivion) {
320
+ background: rgba(240, 245, 255, 0.9);
321
+ }
322
+
323
+ /* Oblivion theme overlay - dark mode */
324
+ :global([data-theme="dark"]) :global(.trackio-modal-overlay.theme--oblivion) {
325
+ background: rgba(15, 20, 30, 0.85);
326
+ }
327
+
328
+ :global(.trackio-modal-overlay.show) {
329
+ opacity: 1;
330
+ pointer-events: auto;
331
+ }
332
+
333
+ :global(.trackio-modal) {
334
+ position: relative;
335
+ width: min(95vw, 1200px);
336
+ backdrop-filter: blur(4px);
337
+
338
+ background: var(--surface-bg);
339
+ border-radius: 12px;
340
+ overflow: hidden;
341
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
342
+ display: flex;
343
+ flex-direction: column;
344
+ }
345
+
346
+ :global(.trackio-modal-header) {
347
+ display: flex;
348
+ justify-content: space-between;
349
+ align-items: center;
350
+ padding: 16px 20px 0px 20px;
351
+ background: var(--surface-bg, white);
352
+ }
353
+
354
+ :global(.trackio-modal-header-left) {
355
+ display: flex;
356
+ align-items: center;
357
+ flex: 1;
358
+ }
359
+
360
+ :global(.trackio-modal-header-right) {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 12px;
364
+ }
365
+
366
+ :global(.trackio-modal-nav-counter-group) {
367
+ display: flex;
368
+ align-items: center;
369
+ gap: 4px;
370
+ }
371
+
372
+ :global(.trackio-modal-counter) {
373
+ font-size: 10px;
374
+ color: var(--muted-color);
375
+ font-family: var(--trackio-font-family);
376
+ font-weight: 500;
377
+ background: none!important;
378
+ border: none!important;
379
+ opacity: 0.6;
380
+ padding: 2px 6px;
381
+ border-radius: 4px;
382
+ line-height: 1;
383
+ }
384
+
385
+ :global(.trackio-modal-header h3) {
386
+ margin: 0;
387
+ font-size: 16px;
388
+ font-weight: 600;
389
+ color: var(--text-color, rgba(0, 0, 0, 0.9));
390
+ flex: 1;
391
+ }
392
+
393
+ :global(.trackio-modal-close) {
394
+ width: 32px;
395
+ height: 32px;
396
+ border: none;
397
+ border-radius: 6px;
398
+ background: transparent;
399
+ color: var(--text-color, rgba(0, 0, 0, 0.7));
400
+ cursor: pointer;
401
+ display: flex;
402
+ align-items: center;
403
+ justify-content: center;
404
+ font-size: 20px;
405
+ transition: background-color 0.15s ease;
406
+ }
407
+
408
+ :global(.trackio-modal-close:hover) {
409
+ background: var(--border-color, rgba(0, 0, 0, 0.1));
410
+ }
411
+
412
+ /* Inline navigation arrows in header */
413
+ :global(.trackio-modal-nav-inline) {
414
+ width: 24px;
415
+ height: 24px;
416
+ border: none;
417
+ border-radius: 4px;
418
+ padding: 0 !important;
419
+ background: transparent;
420
+ color: var(--text-color, rgba(0, 0, 0, 0.7));
421
+ cursor: pointer;
422
+ display: flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ transition: all 0.15s ease;
426
+ flex-shrink: 0;
427
+ }
428
+
429
+ :global(.trackio-modal-nav-inline:hover) {
430
+ background: var(--border-color, rgba(0, 0, 0, 0.1));
431
+ color: var(--text-color, rgba(0, 0, 0, 0.9));
432
+ transform: scale(1.1);
433
+ }
434
+
435
+ :global(.trackio-modal-nav-inline:active) {
436
+ transform: scale(0.9);
437
+ }
438
+
439
+ :global(.trackio-modal-nav-inline svg) {
440
+ width: 14px;
441
+ height: 14px;
442
+ fill: currentColor;
443
+ }
444
+
445
+ :global(.trackio-modal-content) {
446
+ flex: 1;
447
+ padding: 20px;
448
+ overflow: hidden;
449
+ display: flex;
450
+ flex-direction: column;
451
+ gap: 16px;
452
+ }
453
+
454
+ :global(.trackio-modal-legend) {
455
+ display: flex;
456
+ justify-content: flex-start;
457
+ align-items: center;
458
+ }
459
+
460
+ :global(.trackio-modal-chart-content) {
461
+ flex: 1;
462
+ position: relative;
463
+ min-height: 0;
464
+ }
465
+
466
+ /* Ghost hover effect */
467
+ :global(.trackio-modal .ghost) {
468
+ opacity: 0.2;
469
+ transition: opacity 0.15s ease;
470
+ }
471
+
472
+ /* Specific ghost effect for raw lines when smoothing is active */
473
+ :global(.trackio-modal.hovering path.raw-line.ghost) {
474
+ opacity: 0.1;
475
+ }
476
+
477
+ /* =========================
478
+ OBLIVION THEME STYLES
479
+ ========================= */
480
+
481
+ /* Oblivion modal overlay */
482
+ :global(.trackio-modal-overlay.theme--oblivion) {
483
+ background: rgba(15, 17, 21, 0.9);
484
+ }
485
+
486
+ /* Oblivion modal box - styled like a cell with corners */
487
+ :global(.theme--oblivion .trackio-modal) {
488
+ position: relative;
489
+ background: transparent;
490
+ border: none;
491
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
492
+ backdrop-filter: blur(8px);
493
+ backdrop-filter: saturate(1.1) blur(15px);
494
+ }
495
+
496
+ /* Modal background layer (like cell-bg) */
497
+ :global(.theme--oblivion .trackio-modal)::before {
498
+ content: "";
499
+ position: absolute;
500
+ pointer-events: none;
501
+ z-index: 1;
502
+ border-radius: 4px;
503
+ background:
504
+ radial-gradient(1200px 200px at 20% -10%, rgba(0,0,0,.05), transparent 80%),
505
+ radial-gradient(900px 200px at 80% 110%, rgba(0,0,0,.05), transparent 80%);
506
+ backdrop-filter: blur(10px);
507
+ }
508
+
509
+ /* Dark mode oblivion modal */
510
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal)::before {
511
+ background:
512
+ radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, #ffffff 6.5%, transparent), transparent 80%),
513
+ radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, #ffffff 6%, transparent), transparent 80%);
514
+ /* linear-gradient(180deg, color-mix(in srgb, #ffffff 3.5%, transparent), transparent 45%); */
515
+ backdrop-filter: blur(10px);
516
+ }
517
+
518
+ /* Dark mode: bright corners */
519
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal)::after {
520
+ background:
521
+ linear-gradient(#ffffff, #ffffff) top left / 8px 1px no-repeat,
522
+ linear-gradient(#ffffff, #ffffff) top left / 1px 8px no-repeat,
523
+ linear-gradient(#ffffff, #ffffff) top right / 8px 1px no-repeat,
524
+ linear-gradient(#ffffff, #ffffff) top right / 1px 8px no-repeat,
525
+ linear-gradient(#ffffff, #ffffff) bottom left / 8px 1px no-repeat,
526
+ linear-gradient(#ffffff, #ffffff) bottom left / 1px 8px no-repeat,
527
+ linear-gradient(#ffffff, #ffffff) bottom right / 8px 1px no-repeat,
528
+ linear-gradient(#ffffff, #ffffff) bottom right / 1px 8px no-repeat;
529
+ }
530
+
531
+ /* Modal content above pseudo-elements */
532
+ :global(.theme--oblivion .trackio-modal-header),
533
+ :global(.theme--oblivion .trackio-modal-content) {
534
+ position: relative;
535
+ z-index: 5;
536
+ }
537
+
538
+ /* Oblivion modal header */
539
+ :global(.theme--oblivion .trackio-modal-header) {
540
+ background: transparent;
541
+ }
542
+
543
+ :global(.theme--oblivion .trackio-modal-header h3) {
544
+ color: var(--trackio-oblivion-primary, #2a2a2a);
545
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
546
+ font-weight: 800;
547
+ letter-spacing: 0.12em;
548
+ text-transform: uppercase;
549
+ font-size: 14px;
550
+ }
551
+
552
+ :global(.theme--oblivion .trackio-modal-counter) {
553
+ background: var(--trackio-oblivion-dim, rgba(42, 42, 42, 0.3));
554
+ color: var(--trackio-oblivion-primary, #2a2a2a);
555
+ border: 1px solid var(--trackio-oblivion-dim, rgba(42, 42, 42, 0.3));
556
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
557
+ font-weight: 600;
558
+ letter-spacing: 0.08em;
559
+ }
560
+
561
+ :global(.theme--oblivion .trackio-modal-close) {
562
+ color: var(--trackio-oblivion-primary, #2a2a2a);
563
+ background: transparent;
564
+ border: 1px solid transparent;
565
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
566
+ }
567
+
568
+ :global(.theme--oblivion .trackio-modal-close:hover) {
569
+ background: var(--trackio-oblivion-dim, rgba(42, 42, 42, 0.3));
570
+ border: 1px solid var(--trackio-oblivion-dim, rgba(42, 42, 42, 0.3));
571
+ }
572
+
573
+ /* Oblivion inline navigation arrows */
574
+ :global(.theme--oblivion .trackio-modal-nav-inline) {
575
+ background: transparent;
576
+ border: none;
577
+ color: var(--trackio-oblivion-primary, #2a2a2a);
578
+ border-radius: 4px;
579
+ }
580
+
581
+ :global(.theme--oblivion .trackio-modal-nav-inline:hover) {
582
+ background: var(--trackio-oblivion-dim, rgba(42, 42, 42, 0.3));
583
+ transform: scale(1.1);
584
+ }
585
+
586
+ /* Dark mode overrides for modal content */
587
+
588
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal-header h3) {
589
+ color: #ffffff;
590
+ }
591
+
592
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal-counter) {
593
+ background: color-mix(in srgb, #ffffff 25%, transparent);
594
+ color: #ffffff;
595
+ border: 1px solid color-mix(in srgb, #ffffff 25%, transparent);
596
+ }
597
+
598
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal-close) {
599
+ color: #ffffff;
600
+ }
601
+
602
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal-close:hover) {
603
+ background: color-mix(in srgb, #ffffff 25%, transparent);
604
+ border: 1px solid color-mix(in srgb, #ffffff 25%, transparent);
605
+ }
606
+
607
+ /* Dark mode inline navigation arrows */
608
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal-nav-inline) {
609
+ background: transparent;
610
+ border: none;
611
+ color: #ffffff;
612
+ }
613
+
614
+ :global([data-theme="dark"]) :global(.theme--oblivion .trackio-modal-nav-inline:hover) {
615
+ background: color-mix(in srgb, #ffffff 25%, transparent);
616
+ transform: scale(1.1);
617
+ }
618
+ </style>
app/src/components/trackio/Legend.svelte CHANGED
@@ -1,13 +1,28 @@
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}>
@@ -26,16 +41,32 @@
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,
@@ -50,20 +81,45 @@
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;
 
1
  <script>
2
  import { createEventDispatcher } from 'svelte';
3
+
4
+ // Props
5
  export let items = [];
6
+ export let alignment = 'center'; // 'left', 'center', 'right' - Controls the alignment of the legend
7
+
8
+ // Events
9
  const dispatch = createEventDispatcher();
10
  function enter(name){ dispatch('legend-hover', { name }); }
11
  function leave(){ dispatch('legend-leave'); }
12
+
13
+ /**
14
+ * Usage examples:
15
+ * <Legend items={runs} alignment="left" />
16
+ * <Legend items={runs} alignment="center" />
17
+ * <Legend items={runs} alignment="right" />
18
+ */
19
  </script>
20
 
21
+ <div class="legend-bottom legend-align-{alignment}">
22
+ <div class="legend-title">
23
+ Runs
24
+ <span class="legend-count">({items.length})</span>
25
+ </div>
26
  <div class="items">
27
  {#each items as it}
28
  <div class="item" role="presentation" data-run={it.name} on:mouseenter={() => enter(it.name)} on:mouseleave={leave}>
 
41
  .legend-bottom {
42
  display: flex;
43
  flex-direction: column;
 
44
  gap: 6px;
45
  font-size: 12px;
 
46
  width: 80%;
 
47
  color: var(--trackio-legend-text);
48
  font-family: var(--trackio-font-family);
49
  }
50
 
51
+ /* Alignment variations */
52
+ .legend-align-left {
53
+ align-items: flex-start;
54
+ text-align: left;
55
+ margin: 0 auto 0 0;
56
+ }
57
+
58
+ .legend-align-center {
59
+ align-items: center;
60
+ text-align: center;
61
+ margin: 0 auto;
62
+ }
63
+
64
+ .legend-align-right {
65
+ align-items: flex-end;
66
+ text-align: right;
67
+ margin: 0 0 0 auto;
68
+ }
69
+
70
  /* Force Roboto Mono in Oblivion theme */
71
  :global(.trackio.theme--oblivion) .legend-bottom,
72
  :global(.trackio.theme--oblivion) .legend-title,
 
81
  text-transform: uppercase !important;
82
  }
83
 
84
+ :global(.trackio.theme--oblivion) .legend-count {
85
+ font-weight: 500 !important;
86
+ text-transform: none !important;
87
+ letter-spacing: 0.02em !important;
88
+ }
89
+
90
  .legend-title {
91
  font-size: 12px;
92
  font-weight: 700;
93
  color: var(--trackio-legend-text);
94
  }
95
 
96
+ .legend-count {
97
+ font-size: 10px;
98
+ font-weight: 400;
99
+ opacity: 0.6;
100
+ margin-left: 2px;
101
+ }
102
+
103
  .items {
104
  display: flex;
105
  flex-wrap: wrap;
106
  gap: 8px 14px;
 
107
  align-items: center;
108
  }
109
 
110
+ /* Items alignment */
111
+ .legend-align-left .items {
112
+ justify-content: flex-start;
113
+ }
114
+
115
+ .legend-align-center .items {
116
+ justify-content: center;
117
+ }
118
+
119
+ .legend-align-right .items {
120
+ justify-content: flex-end;
121
+ }
122
+
123
  .item {
124
  display: inline-flex;
125
  align-items: center;
app/src/components/trackio/Tooltip.svelte CHANGED
@@ -66,7 +66,7 @@
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
  }
 
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: none;
70
  z-index: var(--z-tooltip, 50);
71
  backdrop-filter: saturate(1.12) blur(8px);
72
  }
app/src/components/trackio/Trackio.svelte CHANGED
@@ -4,6 +4,7 @@
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
 
@@ -175,6 +176,34 @@
175
 
176
 
177
  // Chart logic now handled by Cell.svelte
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  let cleanup = null;
180
  onMount(() => {
@@ -296,6 +325,7 @@
296
  // Ghost the chart lines and points
297
  hostEl.querySelectorAll('.cell').forEach(cell => {
298
  cell.querySelectorAll('svg .lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
 
299
  cell.querySelectorAll('svg .points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
300
  });
301
 
@@ -313,6 +343,7 @@
313
  // Clear ghost from chart lines and points
314
  hostEl.querySelectorAll('.cell').forEach(cell => {
315
  cell.querySelectorAll('svg .lines path.run-line').forEach(p => p.classList.remove('ghost'));
 
316
  cell.querySelectorAll('svg .points circle.pt').forEach(c => c.classList.remove('ghost'));
317
  });
318
 
@@ -329,12 +360,52 @@
329
  <Legend items={legendItems} on:legend-hover={(e) => { const run = e?.detail?.name; if (!run) return; ghostRun(run); }} on:legend-leave={() => { clearGhost(); }} />
330
  </div>
331
  <div class="trackio__grid" bind:this={gridEl}>
332
- {#each cellsDef as c}
333
- <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} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  {/each}
335
  </div>
 
 
 
 
 
 
 
336
  </div>
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  <style>
339
  /* =========================
340
  TRACKIO THEME SYSTEM
@@ -576,6 +647,51 @@
576
  .trackio.theme--oblivion .grid-dots { display: block; }
577
  .trackio.theme--oblivion .cell-bg,
578
  .trackio.theme--oblivion .cell-corners { display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  </style>
580
 
581
 
 
4
  import { generateRunNames, genCurves } from './data-generator.js';
5
  import Legend from './Legend.svelte';
6
  import Cell from './Cell.svelte';
7
+ import FullscreenModal from './FullscreenModal.svelte';
8
  import { onMount, onDestroy } from 'svelte';
9
  import { jitterTrigger } from './store.js';
10
 
 
176
 
177
 
178
  // Chart logic now handled by Cell.svelte
179
+
180
+ // Fullscreen navigation state
181
+ let currentFullscreenIndex = 0;
182
+ let isModalOpen = false;
183
+
184
+ function handleNavigate(newIndex) {
185
+ currentFullscreenIndex = newIndex;
186
+ }
187
+
188
+ function openModal(index) {
189
+ currentFullscreenIndex = index;
190
+ isModalOpen = true;
191
+ }
192
+
193
+ function closeModal() {
194
+ isModalOpen = false;
195
+ }
196
+
197
+ // Prepare all charts data for navigation
198
+ $: allChartsData = cellsDef.map(c => ({
199
+ metricKey: c.metric,
200
+ titleText: c.title,
201
+ metricData: (preparedData && preparedData[c.metric]) || {},
202
+ rawMetricData: (preparedRawData && preparedRawData[c.metric]) || {}
203
+ }));
204
+
205
+ // Color function for the modal
206
+ $: modalColorForRun = (name) => colorsByRun[name] || '#999';
207
 
208
  let cleanup = null;
209
  onMount(() => {
 
325
  // Ghost the chart lines and points
326
  hostEl.querySelectorAll('.cell').forEach(cell => {
327
  cell.querySelectorAll('svg .lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
328
+ cell.querySelectorAll('svg .lines path.raw-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
329
  cell.querySelectorAll('svg .points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
330
  });
331
 
 
343
  // Clear ghost from chart lines and points
344
  hostEl.querySelectorAll('.cell').forEach(cell => {
345
  cell.querySelectorAll('svg .lines path.run-line').forEach(p => p.classList.remove('ghost'));
346
+ cell.querySelectorAll('svg .lines path.raw-line').forEach(p => p.classList.remove('ghost'));
347
  cell.querySelectorAll('svg .points circle.pt').forEach(c => c.classList.remove('ghost'));
348
  });
349
 
 
360
  <Legend items={legendItems} on:legend-hover={(e) => { const run = e?.detail?.name; if (!run) return; ghostRun(run); }} on:legend-leave={() => { clearGhost(); }} />
361
  </div>
362
  <div class="trackio__grid" bind:this={gridEl}>
363
+ {#each cellsDef as c, i}
364
+ <Cell
365
+ metricKey={c.metric}
366
+ titleText={c.title}
367
+ wide={c.wide}
368
+ {variant}
369
+ {normalizeLoss}
370
+ {logScaleX}
371
+ {smoothing}
372
+ metricData={(preparedData && preparedData[c.metric]) || {}}
373
+ rawMetricData={(preparedRawData && preparedRawData[c.metric]) || {}}
374
+ colorForRun={(name)=> colorsByRun[name] || '#999'}
375
+ {hostEl}
376
+ currentIndex={i}
377
+ onOpenModal={openModal}
378
+ />
379
  {/each}
380
  </div>
381
+ <div class="trackio__footer">
382
+ <small>
383
+ Built with <a href="https://github.com/huggingface/trackio" target="_blank" rel="noopener noreferrer">TrackIO</a>
384
+ <span class="separator">•</span>
385
+ <a href="https://huggingface.co/docs/hub/spaces-sdks-docker" target="_blank" rel="noopener noreferrer">Use via API</a>
386
+ </small>
387
+ </div>
388
  </div>
389
 
390
+ <!-- Centralized Fullscreen Modal -->
391
+ <FullscreenModal
392
+ visible={isModalOpen}
393
+ title={allChartsData[currentFullscreenIndex]?.titleText || ''}
394
+ metricData={allChartsData[currentFullscreenIndex]?.metricData || {}}
395
+ rawMetricData={allChartsData[currentFullscreenIndex]?.rawMetricData || {}}
396
+ colorForRun={modalColorForRun}
397
+ {variant}
398
+ {logScaleX}
399
+ {smoothing}
400
+ {normalizeLoss}
401
+ metricKey={allChartsData[currentFullscreenIndex]?.metricKey || ''}
402
+ titleText={allChartsData[currentFullscreenIndex]?.titleText || ''}
403
+ currentIndex={currentFullscreenIndex}
404
+ totalCharts={cellsDef.length}
405
+ onNavigate={handleNavigate}
406
+ on:close={closeModal}
407
+ />
408
+
409
  <style>
410
  /* =========================
411
  TRACKIO THEME SYSTEM
 
647
  .trackio.theme--oblivion .grid-dots { display: block; }
648
  .trackio.theme--oblivion .cell-bg,
649
  .trackio.theme--oblivion .cell-corners { display: block; }
650
+
651
+ /* =========================
652
+ FOOTER
653
+ ========================= */
654
+
655
+ .trackio__footer {
656
+ display: flex;
657
+ justify-content: center;
658
+ align-items: center;
659
+ margin-top: 12px;
660
+ padding-top: 6px;
661
+ opacity: 1;
662
+ }
663
+
664
+ .trackio__footer small {
665
+ font-size: 10px;
666
+ color: var(--trackio-text-secondary);
667
+ font-family: var(--trackio-font-family);
668
+ opacity: 0.7;
669
+ }
670
+
671
+ .trackio__footer a {
672
+ color: var(--trackio-text-secondary);
673
+ text-decoration: none;
674
+ border-top: 1px solid var(--trackio-chart-grid-stroke);
675
+ font-weight: var(--trackio-font-weight-normal);
676
+ transition: opacity 0.15s ease;
677
+ }
678
+
679
+ .trackio__footer a:hover {
680
+ text-decoration: none;
681
+ }
682
+
683
+ .trackio__footer .separator {
684
+ margin: 0 6px;
685
+ }
686
+
687
+ /* Oblivion theme footer adjustments */
688
+ .trackio.theme--oblivion .trackio__footer {
689
+ border-top-color: var(--trackio-oblivion-dim);
690
+ }
691
+
692
+ .trackio.theme--oblivion .trackio__footer small {
693
+ font-family: 'Roboto Mono', 'Roboto Mono Fallback', ui-monospace, SFMono-Regular, Menlo, monospace !important;
694
+ }
695
  </style>
696
 
697
 
app/src/components/trackio/themes/_components.css DELETED
@@ -1,561 +0,0 @@
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 DELETED
@@ -1,49 +0,0 @@
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 DELETED
@@ -1,69 +0,0 @@
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 DELETED
@@ -1,73 +0,0 @@
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 DELETED
@@ -1,106 +0,0 @@
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 DELETED
@@ -1,41 +0,0 @@
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/comparison/id_1_query.png DELETED

Git LFS Details

  • SHA256: feea5ff112a010614d55753cf120048f38a22a12962cec9a7c9b7efae971c52c
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
app/src/content/assets/data/comparison/id_1_rank_1_sim_1.000.png DELETED

Git LFS Details

  • SHA256: e8e932d36f10201ebeca11747a61547d0f752c27fc1f908c3a22eef113975e22
  • Pointer size: 131 Bytes
  • Size of remote file: 313 kB
app/src/content/assets/data/comparison/id_1_rank_2_sim_0.165.png DELETED

Git LFS Details

  • SHA256: 71165c6b2d6d255e190e1ef5a3581402cf7fff55bc977f6a892e6f5fcc782884
  • Pointer size: 131 Bytes
  • Size of remote file: 426 kB
app/src/content/assets/data/comparison/id_1_rank_3_sim_0.143.png DELETED

Git LFS Details

  • SHA256: f4f4422d8fdb249acc6fd6e25307d88c759769e302f8a28726a7751162763c99
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB
app/src/content/assets/data/comparison/id_2_rank_1_sim_1.000.png DELETED

Git LFS Details

  • SHA256: e91e4eaddf3bf08798880367000cfb070e0efbce39c3305d712834e2217b45b4
  • Pointer size: 130 Bytes
  • Size of remote file: 31.8 kB
app/src/content/assets/data/comparison/id_2_rank_2_sim_0.978.png DELETED

Git LFS Details

  • SHA256: ada0f0d0f2e65a3c83db32af1fcfb02c8f69c5a38f63d59a9cc2a0f0bbf1a3d7
  • Pointer size: 130 Bytes
  • Size of remote file: 33 kB
app/src/content/assets/data/comparison/id_2_rank_3_sim_0.975.png DELETED

Git LFS Details

  • SHA256: 9b68b4348567c954f1bf9e70aa3f5f867027ae63a1aa8e45651de62632f54677
  • Pointer size: 130 Bytes
  • Size of remote file: 32.1 kB
app/src/content/assets/data/comparison/id_3_query.png DELETED

Git LFS Details

  • SHA256: f537ec58e2ea8a6ddb8c820fb0a94e2f6d19dd539eb260f0699459f21b9f5c4b
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
app/src/content/assets/data/comparison/id_3_rank_1_sim_0.936.png DELETED

Git LFS Details

  • SHA256: ae9f919d75c7342fcc11870938c37a29cede69ba2b906e3e494c114421c0e0b5
  • Pointer size: 131 Bytes
  • Size of remote file: 210 kB
app/src/content/assets/data/comparison/id_3_rank_2_sim_0.686.png DELETED

Git LFS Details

  • SHA256: 5ef2e71424cab95f0b178d47ef45ef61e8ef6ae7f9bff733066f5c1fd868dbcf
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
app/src/content/assets/data/comparison/id_3_rank_3_sim_0.676.png DELETED

Git LFS Details

  • SHA256: 7ffd4f00b092add67877e19d1ba5d62e5f6748a2eaac6c8d5251c849b9d3e438
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
app/src/content/assets/data/{comparison/id_2_query.png → llm_benchmarks.json} RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:51f839032e3f0483a4e572bde8493f56eda776f535d1770d507bfa3122899e0a
3
- size 18115
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55d9bff23e8f076125669cbbb8011d8cfe282b1efc3e8d7f9332cee4e8cf3f29
3
+ size 1513
app/src/content/chapters/vibe-coding-charts.mdx CHANGED
@@ -72,7 +72,6 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
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
 
 
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 redesign experiment
 
76
  <TrackioWrapper />
77
 
app/src/content/embeds/d3-trackio-oblivion.html DELETED
@@ -1,670 +0,0 @@
1
- <div class="d3-trackio-oblivion">
2
- <div class="d3-trackio-oblivion__grid">
3
- <div class="cell" data-metric="epoch" data-title="epoch"></div>
4
- <div class="cell" data-metric="train_accuracy" data-title="train_accuracy"></div>
5
- <div class="cell" data-metric="train_loss" data-title="train_loss"></div>
6
- <div class="cell" data-metric="val_accuracy" data-title="val_accuracy"></div>
7
- <div class="cell cell--wide" data-metric="val_loss" data-title="val_loss"></div>
8
- </div>
9
- <noscript>JavaScript is required to render this chart.</noscript>
10
- </div>
11
- <style>
12
- @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;600;700&display=swap');
13
- /* Futuristic "Oblivion"-inspired styling */
14
- .d3-trackio-oblivion { position: relative;
15
- --cell-gap: 0px;
16
- /* Default: light/neutral theme */
17
- --obl-base: #323232; /* base hue for the UI (light theme) */
18
- --obl-cyan: var(--obl-base);
19
- --obl-cyan-dim: color-mix(in srgb, var(--obl-base) 28%, transparent);
20
- --obl-bg: color-mix(in srgb, var(--obl-base) 4%, transparent);
21
- --obl-border: color-mix(in srgb, var(--obl-base) 22%, transparent);
22
- --ghost-obl-border: color-mix(in srgb, var(--obl-base) 6%, transparent);
23
- --obl-glow: 0 0 0 1px color-mix(in srgb, var(--obl-base) 5%, transparent), 0 8px 40px color-mix(in srgb, var(--obl-base) 6%, transparent);
24
- /* Engraved separator colors (light theme) */
25
- --engrave-light: color-mix(in srgb, white 15%, transparent);
26
- --engrave-dark: color-mix(in srgb, var(--obl-base) 10%, transparent);
27
- background: transparent;
28
- --corner-inset: 6px;
29
- --hud-gap: 10px;
30
- --hud-corner-size: 8px;
31
- /* Chart background gradient as a variable for easy theming */
32
- --hud-bg-gradient: radial-gradient(1200px 200px at 20% -10%, rgba(0,0,0,.05), transparent 80%), radial-gradient(900px 200px at 80% 110%, rgba(0,0,0,.05), transparent 80%);
33
- /* Tooltip offset (bottom-right of cursor) */
34
- --tip-offset-x: 10px;
35
- --tip-offset-y: 10px;
36
- padding: var(--hud-gap);
37
- --z-tooltip: 50;
38
- --z-overlay: 99999999;
39
- z-index: var(--z-tooltip);
40
- font-family: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
41
- }
42
- .d3-trackio-oblivion * { font-family: inherit;
43
- }
44
- /* Dark mode: cyan/Oblivion theme */
45
- [data-theme="dark"] .d3-trackio-oblivion {
46
- --obl-base: #ffffff; /* base hue for the UI (dark theme) */
47
- --obl-cyan: var(--obl-base);
48
- --obl-cyan-dim: color-mix(in srgb, var(--obl-base) 25%, transparent);
49
- --obl-bg: color-mix(in srgb, var(--obl-base) 6%, transparent);
50
- --obl-border: color-mix(in srgb, var(--obl-base) 25%, transparent);
51
- --ghost-obl-border: color-mix(in srgb, var(--obl-base) 2%, transparent);
52
- --obl-glow: 0 0 0 1px color-mix(in srgb, var(--obl-base) 35%, transparent), 0 8px 40px color-mix(in srgb, var(--obl-base) 12%, transparent);
53
- /* Richer cyan gradients for the classic Oblivion blue feel */
54
- --hud-bg-gradient:
55
- radial-gradient(1400px 260px at 20% -10%, color-mix(in srgb, var(--obl-base) 6.5%, transparent), transparent 80%),
56
- radial-gradient(1100px 240px at 80% 110%, color-mix(in srgb, var(--obl-base) 6%, transparent), transparent 80%),
57
- linear-gradient(180deg, color-mix(in srgb, var(--obl-base) 3.5%, transparent), transparent 45%);
58
- /* Engraved separator colors (dark theme) */
59
- --engrave-light: color-mix(in srgb, var(--obl-base) 5%, transparent);
60
- --engrave-dark: color-mix(in srgb, black 15%, transparent);
61
- background: #0f1115;
62
- }
63
- /* Tooltip shadows: neutral by default, bluish tint in dark */
64
- .d3-trackio-oblivion .d3-tooltip { box-shadow: 0 8px 32px color-mix(in srgb, var(--obl-base) 8%, transparent), 0 2px 8px color-mix(in srgb, var(--obl-base) 6%, transparent); }
65
- [data-theme="dark"] .d3-trackio-oblivion .d3-tooltip { box-shadow: 0 8px 32px color-mix(in srgb, var(--obl-base) 5%, transparent), 0 2px 8px color-mix(in srgb, black 10%, transparent); }
66
-
67
- .d3-trackio-oblivion__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: --cell-gap; }
68
- @media (max-width: 980px) { .d3-trackio-oblivion__grid { grid-template-columns: 1fr; } }
69
- .d3-trackio-oblivion__grid .cell--wide { grid-column: 1 / -1; }
70
-
71
- .d3-trackio-oblivion .cell {
72
- /* border: 1px solid var(--obl-border);
73
- border-radius: 4px;
74
- background: var(--obl-bg);
75
- backdrop-filter: saturate(1.1) blur(10px);
76
- box-shadow: var(--obl-glow); */
77
- display: flex;
78
- flex-direction: column;
79
- position: relative;
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,
91
- linear-gradient(var(--obl-cyan), var(--obl-cyan)) top right / var(--hud-corner-size) 1px no-repeat,
92
- linear-gradient(var(--obl-cyan), var(--obl-cyan)) top right / 1px var(--hud-corner-size) no-repeat,
93
- linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom left / var(--hud-corner-size) 1px no-repeat,
94
- linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom left / 1px var(--hud-corner-size) no-repeat,
95
- linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom right / var(--hud-corner-size) 1px no-repeat,
96
- linear-gradient(var(--obl-cyan), var(--obl-cyan)) bottom right / 1px var(--hud-corner-size) no-repeat; opacity:.85; }
97
- .d3-trackio-oblivion .cell-inner { position: relative; z-index: 2; padding: var(--hud-corner-size) 12px 10px var(--hud-gap); display:flex; flex-direction:column; }
98
- .d3-trackio-oblivion .cell-header { padding: 10px 6px 10px 12px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
99
- .d3-trackio-oblivion .cell-title { position: relative; font-size: 12px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: var(--obl-cyan); padding-left: 14px; }
100
- .d3-trackio-oblivion .cell-title:before { content:""; position:absolute; left:0; top:50%; transform:translateY(-50%); width: 6px; height: 6px; background: var(--obl-cyan); border: 1px solid var(--obl-border); box-shadow: 0 0 10px color-mix(in srgb, var(--obl-base) 25%, transparent) inset; opacity: .5; }
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; }
108
- .d3-trackio-oblivion .cell-body svg { max-width: 100%; height: auto; display: block; }
109
-
110
- /* Axes & grid use cyan tint */
111
- .d3-trackio-oblivion .axes path, .d3-trackio-oblivion .axes line { stroke: var(--obl-cyan-dim); stroke-opacity: 1; }
112
- .d3-trackio-oblivion .axes text { fill: var(--obl-cyan); opacity: .6; font-weight: 400; letter-spacing: 0.02em; }
113
- .d3-trackio-oblivion .grid line { stroke: var(--obl-cyan-dim); stroke-opacity: .5; }
114
-
115
- /* Legend header */
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
-
123
- /* Hover ghosting */
124
- .d3-trackio-oblivion.hovering .lines path.ghost { opacity: .22; }
125
- .d3-trackio-oblivion.hovering .points circle.ghost { opacity: .22; }
126
- .d3-trackio-oblivion.hovering .areas path.ghost { opacity: .06; }
127
- .d3-trackio-oblivion.hovering .legend-bottom .item.ghost { opacity: .35; }
128
-
129
- /* Tooltip */
130
- .d3-trackio-oblivion .d3-tooltip {
131
- position: absolute;
132
- top: 0;
133
- left: 0;
134
- transform: translate(-9999px, -9999px);
135
- pointer-events: none;
136
- padding: 10px 12px;
137
- border-radius: 8px;
138
- font-size: 12px;
139
- line-height: 1.35;
140
- background: var(--obl-bg);
141
- color: var(--obl-cyan);
142
- box-shadow: 0 8px 32px rgba(127,241,255,.05), 0 2px 8px rgba(0,0,0,.10);
143
- opacity: .5;
144
- transition: opacity .12s ease;
145
- z-index: 1000;
146
- backdrop-filter: saturate(1.1) blur(10px);
147
- }
148
- .d3-trackio-oblivion .d3-tooltip.is-visible { opacity: 1; }
149
- .d3-trackio-oblivion .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; text-align: left; }
150
- .d3-trackio-oblivion .d3-tooltip__inner > div:first-child { font-weight: 900; letter-spacing: .18em; text-transform: uppercase; color: var(--obl-cyan); }
151
- .d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--obl-cyan); opacity: .4; display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.06em; }
152
- .d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3) { position: relative; padding-top: 10px; margin-top: 6px; }
153
- .d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3)::before { content:""; position:absolute; left:0; right:0; top:0; height:1px; background: var(--engrave-dark); }
154
- .d3-trackio-oblivion .d3-tooltip__inner > div:nth-child(n+3)::after { content:""; position:absolute; left:0; right:0; top:1px; height:1px; background: var(--engrave-light); }
155
- .d3-trackio-oblivion .d3-tooltip__color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 2px; border: 1px solid var(--obl-border); box-shadow: 0 0 10px color-mix(in srgb, var(--obl-base) 20%, transparent) inset; }
156
- </style>
157
- <script>
158
- (() => {
159
- const THIS_SCRIPT = document.currentScript;
160
- const TARGET_METRICS = ['epoch','train_accuracy','train_loss','val_accuracy','val_loss'];
161
-
162
- const prettyMetricLabel = (key) => {
163
- if (!key) return '';
164
- const table = { 'train_accuracy':'Train Accuracy', 'val_accuracy':'Val Accuracy', 'train_loss':'Train Loss', 'val_loss':'Val Loss', 'epoch':'Epoch' };
165
- if (table[key]) return table[key];
166
- const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
167
- return cleaned.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
168
- };
169
-
170
- const ensureD3 = (cb) => {
171
- if (window.d3 && typeof window.d3.select === 'function') return cb();
172
- let s = document.getElementById('d3-cdn-script');
173
- if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
174
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
175
- s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
176
- };
177
-
178
- function initCell(cell){
179
- const d3 = window.d3;
180
- const metricKey = cell.getAttribute('data-metric');
181
- const titleText = cell.getAttribute('data-title') || metricKey;
182
- // Structured layers: bg + corners + inner (header + svg)
183
- const bg = document.createElement('div'); bg.className = 'cell-bg'; cell.appendChild(bg);
184
- const corners = document.createElement('div'); corners.className = 'cell-corners'; cell.appendChild(corners);
185
- const inner = document.createElement('div'); inner.className = 'cell-inner'; cell.appendChild(inner);
186
- const header = document.createElement('div'); header.className = 'cell-header';
187
- const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = prettyMetricLabel(titleText); header.appendChild(title);
188
- // Fullscreen icon button (no click handler here)
189
- const fsBtn = document.createElement('button'); fsBtn.className = 'cell-action cell-action--fullscreen'; fsBtn.type = 'button'; fsBtn.title = 'Fullscreen'; fsBtn.setAttribute('aria-label', 'Open fullscreen');
190
- fsBtn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5v2H6v3H4zm10-5h5v5h-2V6h-3V4zM6 18h3v2H4v-5h2v3zm12-3h2v5h-5v-2h3v-3z"/></svg>';
191
- header.appendChild(fsBtn);
192
- inner.appendChild(header);
193
-
194
- const body = document.createElement('div'); body.className = 'cell-body'; inner.appendChild(body);
195
- const svg = d3.select(body).append('svg').attr('width','100%').style('display','block');
196
- const gRoot = svg.append('g');
197
- const gGrid = gRoot.append('g').attr('class','grid');
198
- const gAxes = gRoot.append('g').attr('class','axes');
199
- const gAreas = gRoot.append('g').attr('class','areas');
200
- const gLines = gRoot.append('g').attr('class','lines');
201
- const gPoints = gRoot.append('g').attr('class','points');
202
- const gHover = gRoot.append('g').attr('class','hover');
203
- const host = cell.closest('.d3-trackio-oblivion');
204
-
205
- // Tooltip
206
- cell.style.position = cell.style.position || 'relative';
207
- let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
208
- if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tip.appendChild(tipInner); cell.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
209
-
210
- // Layout & scales
211
- let width = 800, height = 180; const margin = { top: 12, right: 20, bottom: 36, left: 44 };
212
- let xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
213
- const lineGen = d3.line().x(d => xScale(0)).y(d => yScale(d.value));
214
-
215
- // Generic number abbreviation for axis ticks (K/M/B) with up to 2 decimals
216
- const formatAbbrev = (value) => {
217
- const num = Number(value);
218
- if (!Number.isFinite(num)) return String(value);
219
- const abs = Math.abs(num);
220
- const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, '');
221
- if (abs >= 1e9) return `${trim2(num / 1e9)}B`;
222
- if (abs >= 1e6) return `${trim2(num / 1e6)}M`;
223
- if (abs >= 1e3) return `${trim2(num / 1e3)}K`;
224
- return trim2(num);
225
- };
226
-
227
- function updateLayout(axisLabelY, xTicksArg){
228
- const rect = cell.getBoundingClientRect();
229
- width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
230
- height = 180;
231
- svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
232
- // Respect HUD gap: keep content inside gradient inset
233
- const css = getComputedStyle(cell);
234
- const hudGap = Math.max(0, parseFloat(css.getPropertyValue('--hud-gap')) || 0);
235
- const innerWidth = Math.max(0, width - margin.left - margin.right - hudGap * 2);
236
- const innerHeight = Math.max(0, height - margin.top - margin.bottom - hudGap * 2);
237
- gRoot.attr('transform', `translate(${margin.left + hudGap},${margin.top + hudGap})`);
238
- xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);
239
- // grid cleared; drawn as dot intersections in render()
240
- gGrid.selectAll('*').remove();
241
- gAxes.selectAll('*').remove();
242
- // Build ticks that always include domain edges
243
- const makeTicks = (scale, approx) => {
244
- const arr = scale.ticks(approx);
245
- const dom = scale.domain();
246
- if (arr.length === 0 || arr[0] !== dom[0]) arr.unshift(dom[0]);
247
- if (arr[arr.length - 1] !== dom[dom.length - 1]) arr.push(dom[dom.length - 1]);
248
- // dedupe
249
- return Array.from(new Set(arr));
250
- };
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]]
286
- : Array.from({ length: yCount }, (_, i) => yDom[0] + ((yDom[1] - yDom[0]) * (i / (yCount - 1))));
287
- gAxes
288
- .append('g')
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);
296
- })
297
- )
298
- .call((g) => {
299
- g.selectAll('path.domain').attr('stroke', 'var(--obl-cyan-dim)');
300
- g.selectAll('.tick line')
301
- .attr('stroke', 'var(--obl-cyan-dim)')
302
- .attr('y1', 4)
303
- .attr('y2', 6);
304
- g.selectAll('text')
305
- .attr('fill', 'var(--obl-cyan)')
306
- .style('font-size', '11px');
307
- });
308
-
309
- gAxes
310
- .append('g')
311
- .call(d3.axisLeft(yScale).tickValues(yTicksForced).tickFormat((v) => formatAbbrev(v)))
312
- .call((g) => {
313
- g.selectAll('path.domain').attr('stroke', 'var(--obl-cyan-dim)');
314
- g.selectAll('.tick line')
315
- .attr('stroke', 'var(--obl-cyan-dim)')
316
- .attr('x1', -4)
317
- .attr('x2', -6);
318
- g.selectAll('text')
319
- .attr('fill', 'var(--obl-cyan)')
320
- .style('font-size', '11px');
321
- });
322
- gAxes
323
- .append('text')
324
- .attr('class','x-axis-label')
325
- .attr('x', innerWidth/2)
326
- .attr('y', innerHeight + Math.max(22, Math.min(36, margin.bottom - 2)))
327
- .attr('fill', 'var(--obl-cyan)')
328
- .attr('text-anchor', 'middle')
329
- .style('font-size','9px')
330
- .style('opacity', '0.3')
331
- .style('font-weight','500')
332
- .style('letter-spacing','.12em')
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){
340
- const runs = Object.keys(metricData || {});
341
- const hasAny = runs.some(r => (metricData[r]||[]).length > 0);
342
- if (!hasAny) { gRoot.style('display','none'); let msg = body.querySelector('.empty-msg'); if (!msg) { msg = document.createElement('div'); msg.className='empty-msg'; msg.textContent='Metric not found in data.'; Object.assign(msg.style,{ padding:'10px', fontSize:'12px', color:'var(--obl-cyan)', opacity:.6 }); body.appendChild(msg); } return; }
343
- const msg = body.querySelector('.empty-msg'); if (msg) msg.remove(); gRoot.style('display', null);
344
-
345
- let minStep = Infinity, maxStep = -Infinity, minVal = Infinity, maxVal = -Infinity;
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)));
355
- const hoverSteps = Array.from(new Set(rawSteps)).sort((a,b)=>a-b);
356
- const stepIndex = new Map(hoverSteps.map((s,i)=>[s,i]));
357
- const indices = hoverSteps.map((_, i) => i);
358
- // Linear index scale → ticks at edges, equal spacing
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 });
380
- }
381
- });
382
- });
383
- gGrid.selectAll('circle.grid-dot')
384
- .data(gridPoints)
385
- .join('circle')
386
- .attr('class', 'grid-dot')
387
- .attr('cx', d => xScale(d.sx))
388
- .attr('cy', d => yScale(d.ty))
389
- .attr('r', 1.25)
390
- .attr('fill', 'var(--obl-cyan-dim)')
391
- .attr('fill-opacity', 0.5);
392
-
393
- // No stderr correction shapes for now (kept intentionally minimal)
394
- gAreas.selectAll('*').remove();
395
-
396
- // Lines
397
- const series = runs.map(r => ({ run:r, color: colorForRun(r), values:(metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
398
- const paths = gLines.selectAll('path.run').data(series, d=>d.run);
399
- paths.enter().append('path').attr('class','run').attr('fill','none').attr('stroke-width',1.8).attr('opacity',0.95).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
400
- paths.transition().duration(260).attr('stroke', d=>d.color).attr('opacity',0.95).attr('d', d=>lineGen(d.values));
401
- paths.exit().remove();
402
-
403
- // Points
404
- const allPts = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
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
421
-
422
- // Hover
423
- gHover.selectAll('*').remove();
424
- const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
425
- const hoverLine = gHover.append('line').style('stroke','var(--obl-cyan)').attr('stroke-opacity', 0.35).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
426
- // External hover handlers
427
- cell.__showExternalStep = (stepVal) => {
428
- if (stepVal == null) { hoverLine.style('display','none'); return; }
429
- const idx = stepIndex.get(stepVal);
430
- if (idx == null) { hoverLine.style('display','none'); return; }
431
- const xpx = xScale(idx);
432
- hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
433
- };
434
- cell.__clearExternalStep = () => { hoverLine.style('display','none'); };
435
- if (!cell.__syncAttached && host) {
436
- host.addEventListener('trackio-hover-step', (ev) => { const d = ev && ev.detail; if (!d) return; if (cell.__showExternalStep) cell.__showExternalStep(d.step); });
437
- host.addEventListener('trackio-hover-clear', () => { if (cell.__clearExternalStep) cell.__clearExternalStep(); });
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
-
452
- return { metricKey, render };
453
- }
454
-
455
- const bootstrap = () => {
456
- const scriptEl = THIS_SCRIPT; let host = null; const header = document.createElement('div'); header.className = 'd3-trackio-oblivion__header'; const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Runs</div><div class="items"></div>'; header.appendChild(legend);
457
- if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) host = scriptEl.parentElement.querySelector('.d3-trackio-oblivion'); if (!host) { let sib = scriptEl && scriptEl.previousElementSibling; while (sib && !(sib.classList && sib.classList.contains('d3-trackio-oblivion'))) { sib = sib.previousElementSibling; } host = sib || null; }
458
- if (!host) { host = document.querySelector('.d3-trackio-oblivion'); }
459
- if (!host) return; if (host.dataset && host.dataset.mounted==='true') return; if (host.dataset) host.dataset.mounted='true';
460
-
461
- // Insert legend header above the grid container
462
- const gridNode = host.querySelector('.d3-trackio-oblivion__grid');
463
- if (gridNode && gridNode.parentNode === host) { host.insertBefore(header, gridNode); } else { host.insertBefore(header, host.firstChild); }
464
- const cells = Array.from(host.querySelectorAll('.cell')); if (!cells.length) return;
465
- const instances = cells.map(cell => initCell(cell));
466
-
467
- // Read HtmlEmbed attributes
468
- let mountEl = host; while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) { mountEl = mountEl.parentElement; }
469
- let providedData = null; let providedConfig = null;
470
- try { const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null; if (attr && attr.trim()) providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim(); } catch(_){ }
471
- try { const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; if (cfg && cfg.trim()) providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; } catch(_){ }
472
-
473
- const DEFAULT_CSV = '/data/trackio_wandb_demo.csv';
474
- const ensureDataPrefix = (p) => { if (typeof p !== 'string' || !p) return p; return p.includes('/') ? p : `/data/${p}`; };
475
- const normalizeInput = (inp) => Array.isArray(inp) ? inp.map(ensureDataPrefix) : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
476
- const CSV_PATHS = Array.isArray(providedData) ? normalizeInput(providedData) : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [ DEFAULT_CSV, './assets/data/trackio_wandb_demo.csv', '../assets/data/trackio_wandb_demo.csv', '../../assets/data/trackio_wandb_demo.csv' ]);
477
-
478
- const d3 = window.d3;
479
- (async () => {
480
- try {
481
- const texts = await Promise.all(CSV_PATHS.map(p => fetch(p, { cache:'no-cache' }).then(r => r.ok ? r.text() : '').catch(()=>'')));
482
- const textAll = texts.filter(Boolean).join('\n');
483
- const rows = d3.csvParse(textAll, d => ({ run:(d.run||'').trim(), step:+d.step, metric:(d.metric||'').trim(), value:+d.value, stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null }));
484
- // Filter out comments and invalid rows before building lists/maps
485
- const cleanRows = rows.filter(r => {
486
- const run = String(r.run||'').trim();
487
- const metric = String(r.metric||'').trim();
488
- return run && run.charAt(0) !== '#' && metric && isFinite(+r.step) && !isNaN(+r.step) && isFinite(+r.value) && !isNaN(+r.value);
489
- });
490
- const metricsInData = Array.from(new Set(cleanRows.map(r => r.metric)));
491
- const lcSet = new Set(metricsInData.map(m => m.toLowerCase()));
492
- const preferIfExists = (cand) => cand.find(c => lcSet.has(String(c).toLowerCase())) || null;
493
- const resolveMetric = (target) => {
494
- const override = providedConfig && providedConfig.metricMap && providedConfig.metricMap[target]; if (override && lcSet.has(String(override).toLowerCase())) return metricsInData.find(m => m.toLowerCase() === String(override).toLowerCase());
495
- const exact = metricsInData.find(m => m.toLowerCase() === target.toLowerCase()); if (exact) return exact;
496
- const cands = (name) => metricsInData.filter(m => m.toLowerCase().includes(name));
497
- if (target === 'epoch') return preferIfExists(['epoch']);
498
- if (target === 'train_accuracy') return preferIfExists(['train_accuracy','training_accuracy','accuracy_train','train_acc','acc_train','train/accuracy','accuracy']) || cands('acc').find(m => /train|trn/i.test(m));
499
- if (target === 'val_accuracy') return preferIfExists(['val_accuracy','valid_accuracy','validation_accuracy','val_acc','acc_val','val/accuracy']) || cands('acc').find(m => /val|valid/i.test(m));
500
- if (target === 'train_loss') return preferIfExists(['train_loss','training_loss','loss_train','train/loss','loss']) || cands('loss').find(m => /train|trn/i.test(m));
501
- if (target === 'val_loss') return preferIfExists(['val_loss','validation_loss','valid_loss','loss_val','val/loss']) || cands('loss').find(m => /val|valid/i.test(m));
502
- return null;
503
- };
504
- const TARGET_TO_DATA = Object.fromEntries(TARGET_METRICS.map(t => [t, resolveMetric(t)]));
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); });
528
-
529
- instances.forEach(inst => { const metricMap = dataByMetric.get(inst.metricKey) || {}; inst.render(metricMap, colorForRun); });
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
- }
663
- })();
664
- };
665
-
666
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
667
- })();
668
- </script>
669
-
670
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-trackio.html DELETED
@@ -1,843 +0,0 @@
1
- <div class="d3-trackio">
2
- <div class="d3-trackio__grid">
3
- <div class="cell" data-metric="epoch" data-title="epoch"></div>
4
- <div class="cell" data-metric="train_accuracy" data-title="train_accuracy"></div>
5
- <div class="cell" data-metric="train_loss" data-title="train_loss"></div>
6
- <div class="cell" data-metric="val_accuracy" data-title="val_accuracy"></div>
7
- <div class="cell cell--wide" data-metric="val_loss" data-title="val_loss"></div>
8
- </div>
9
- <noscript>JavaScript is required to render this chart.</noscript>
10
- </div>
11
- <style>
12
- .d3-trackio { position: relative; --z-tooltip: 50; --z-overlay: 99999999;
13
- /* Softer chart theming (light) */
14
- --axis-color: rgba(0,0,0,.18);
15
- --tick-color: rgba(0,0,0,.50);
16
- --grid-color: rgba(0,0,0,.05);
17
- }
18
- /* Softer chart theming (dark) */
19
- [data-theme="dark"] .d3-trackio {
20
- --axis-color: rgba(255,255,255,.18);
21
- --tick-color: rgba(255,255,255,.50);
22
- --grid-color: rgba(255,255,255,.05);
23
- }
24
- .d3-trackio__grid {
25
- display: grid;
26
- grid-template-columns: repeat(2, minmax(0, 1fr));
27
- gap: 12px;
28
- }
29
- @media (max-width: 980px) {
30
- .d3-trackio__grid { grid-template-columns: 1fr; }
31
- }
32
- .d3-trackio__grid .cell--wide { grid-column: 1 / -1; }
33
-
34
- .d3-trackio .cell {
35
- border: 1px solid var(--border-color);
36
- border-radius: 10px;
37
- background: var(--surface-bg);
38
- display: flex;
39
- flex-direction: column;
40
- position: relative;
41
- }
42
- .d3-trackio .cell-header {
43
- padding: 8px 10px;
44
- display: flex;
45
- align-items: center;
46
- justify-content: space-between;
47
- gap: 8px;
48
- }
49
- .d3-trackio .cell-title {
50
- font-size: 13px;
51
- font-weight: 700;
52
- color: var(--text-color);
53
- text-transform: none;
54
- }
55
- .d3-trackio .cell-action {
56
- margin-left: auto;
57
- display: inline-flex;
58
- align-items: center;
59
- justify-content: center;
60
- width: 36px;
61
- height: 36px;
62
- border: 0;
63
- background: transparent;
64
- color: var(--text-color);
65
- opacity: .8;
66
- cursor: pointer;
67
- }
68
- .d3-trackio .cell-action:hover { opacity: 1; }
69
- .d3-trackio .cell-action svg { width: 28px; height: 28px; }
70
- .d3-trackio .cell-action svg, .d3-trackio .cell-action svg path { fill: var(--text-color); stroke: none; }
71
-
72
- /* Fullscreen overlay */
73
- .d3-trackio__overlay {
74
- position: fixed;
75
- inset: 0;
76
- background: rgba(0,0,0,.48);
77
- display: flex;
78
- align-items: center;
79
- justify-content: center;
80
- z-index: var(--z-overlay);
81
- opacity: 0;
82
- pointer-events: none;
83
- transition: opacity .2s ease;
84
- }
85
- .d3-trackio__overlay.is-open { opacity: 1; pointer-events: auto; }
86
- .d3-trackio__modal {
87
- position: relative;
88
- width: min(92vw, 1200px);
89
- height: min(92vh, 900px);
90
- display: flex;
91
- align-items: stretch;
92
- justify-content: stretch;
93
- transform: scale(.96);
94
- transition: transform .2s ease;
95
- }
96
- .d3-trackio__overlay.is-open .d3-trackio__modal { transform: scale(1); }
97
- .d3-trackio__modal .cell { width: 100%; height: 100%; }
98
- .d3-trackio__modal .cell .cell-body { height: calc(100% - 44px); }
99
- /* Conserver le ratio pour éviter les décalages points/ticks lors du zoom */
100
- .d3-trackio__modal .cell .cell-body svg { width: 100% !important; height: auto !important; }
101
- .d3-trackio__modal .cell .cell-action { display: none; }
102
- .d3-trackio__modal-close {
103
- position: absolute;
104
- top: 10px;
105
- right: 10px;
106
- background: var(--surface-bg);
107
- border: 1px solid var(--border-color);
108
- color: var(--text-color);
109
- width: 28px;
110
- height: 28px;
111
- display: flex;
112
- align-items: center;
113
- justify-content: center;
114
- cursor: pointer;
115
- }
116
- .d3-trackio .cell-body { position: relative; width: 100%; overflow: hidden; }
117
- .d3-trackio .cell-body svg { max-width: 100%; height: auto; display: block; }
118
-
119
- /* Axes/grid colors via project CSS variables */
120
- .d3-trackio .axes path,
121
- .d3-trackio .axes line { stroke: var(--axis-color); }
122
- .d3-trackio .axes text { fill: var(--tick-color); }
123
- .d3-trackio .grid line { stroke: var(--grid-color); }
124
-
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;
132
- flex-wrap: wrap;
133
- width: 100%;
134
- }
135
- .d3-trackio__header .legend-bottom {
136
- display: flex;
137
- flex-direction: column;
138
- align-items: center;
139
- gap: 6px;
140
- font-size: 12px;
141
- color: var(--text-color);
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
-
149
- /* Hover ghosting */
150
- .d3-trackio.hovering .lines path.ghost { opacity: .25; }
151
- .d3-trackio.hovering .points circle.ghost { opacity: .25; }
152
- .d3-trackio.hovering .areas path.ghost { opacity: .08; }
153
- .d3-trackio.hovering .legend-bottom .item.ghost { opacity: .35; }
154
-
155
- /* Tooltip styling aligned with other embeds */
156
- .d3-trackio .d3-tooltip {
157
- z-index: var(--z-tooltip);
158
- backdrop-filter: saturate(1.12) blur(8px);
159
- }
160
- .d3-trackio .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; }
161
- .d3-trackio .d3-tooltip__inner > div:first-child { font-weight: 800; letter-spacing: 0.1px; margin-bottom: 0; }
162
- .d3-trackio .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; }
163
- .d3-trackio .d3-tooltip__inner > div:nth-child(n+3) { padding-top: 6px; border-top: 1px solid var(--border-color); }
164
- .d3-trackio .d3-tooltip__color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-color); }
165
- </style>
166
- <script>
167
- (() => {
168
- const THIS_SCRIPT = document.currentScript;
169
-
170
- const TARGET_METRICS = [
171
- 'epoch',
172
- 'train_accuracy',
173
- 'train_loss',
174
- 'val_accuracy',
175
- 'val_loss'
176
- ];
177
-
178
- const prettyMetricLabel = (key) => {
179
- if (!key) return '';
180
- const table = {
181
- 'train_accuracy': 'Train Accuracy',
182
- 'val_accuracy': 'Val Accuracy',
183
- 'train_loss': 'Train Loss',
184
- 'val_loss': 'Val Loss',
185
- 'epoch': 'Epoch'
186
- };
187
- if (table[key]) return table[key];
188
- const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
189
- return cleaned.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
190
- };
191
-
192
- const ensureD3 = (cb) => {
193
- if (window.d3 && typeof window.d3.select === 'function') return cb();
194
- let s = document.getElementById('d3-cdn-script');
195
- if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
196
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
197
- s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
198
- };
199
-
200
- // Build per-cell line chart
201
- function initCell(cell) {
202
- const d3 = window.d3;
203
- const metricKey = cell.getAttribute('data-metric');
204
- const titleText = cell.getAttribute('data-title') || metricKey;
205
- const host = cell.closest('.d3-trackio');
206
- const shouldSuppress = () => !!(host && host.dataset && host.dataset.suppressTransitions === '1');
207
-
208
- // Header
209
- const header = document.createElement('div'); header.className = 'cell-header';
210
- const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = prettyMetricLabel(titleText); header.appendChild(title);
211
- cell.appendChild(header);
212
-
213
- // Body & SVG
214
- const body = document.createElement('div'); body.className = 'cell-body'; cell.appendChild(body);
215
- const svg = d3.select(body).append('svg').attr('width','100%').style('display','block');
216
- const gRoot = svg.append('g');
217
- const gGrid = gRoot.append('g').attr('class','grid');
218
- const gAxes = gRoot.append('g').attr('class','axes');
219
- const gAreas = gRoot.append('g').attr('class','areas');
220
- const gLines = gRoot.append('g').attr('class','lines');
221
- const gPoints = gRoot.append('g').attr('class','points');
222
- const gHover = gRoot.append('g').attr('class','hover');
223
-
224
- // Legacy flag, kept for compatibility but computed via shouldSuppress()
225
- let suppressTransitions = false;
226
-
227
- // Tooltip
228
- cell.style.position = cell.style.position || 'relative';
229
- let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
230
- if (!tip) {
231
- tip = document.createElement('div'); tip.className = 'd3-tooltip';
232
- Object.assign(tip.style, {
233
- position:'absolute', top:'0', left:'0', transform:'translate(-9999px,-9999px)', pointerEvents:'none',
234
- padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
235
- background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease'
236
- });
237
- tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); cell.appendChild(tip);
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));
245
-
246
- function updateLayout(axisLabelY, xTicksArg){
247
- const rect = cell.getBoundingClientRect();
248
- width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
249
- // Hauteur fixe par défaut, overridable via data-height (pour fullscreen)
250
- height = Number(cell.getAttribute('data-height')) || 200;
251
- svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
252
- const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom;
253
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
254
- xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);
255
-
256
- // Grid
257
- gGrid.selectAll('*').remove();
258
- gGrid.selectAll('line').data(yScale.ticks(6)).join('line')
259
- .attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))
260
- .attr('stroke','var(--grid-color)').attr('stroke-width',1).attr('shape-rendering','crispEdges');
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();
270
- if (arr.length === 0 || arr[0] !== dom[0]) arr.unshift(dom[0]);
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]]
304
- : Array.from({ length: yCount }, (_, i) => yDom[0] + ((yDom[1] - yDom[0]) * (i / (yCount - 1))));
305
- // X axis with forced ticks (indices) and formatted labels mapped to original steps in render()
306
- gAxes.append('g')
307
- .attr('transform', `translate(0,${innerHeight})`)
308
- .call(
309
- d3.axisBottom(xScale)
310
- .tickValues(xTicksForced)
311
- .tickFormat((i) => {
312
- // Label mapping will be provided by caller via closure over xTicksArg
313
- const val = (Array.isArray(xTicksArg) && xTicksArg[i] != null ? xTicksArg[i] : i);
314
- return formatAbbrev(val);
315
- })
316
- )
317
- .call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','11px'); });
318
- // Y axis with forced ticks
319
- gAxes.append('g')
320
- .call(d3.axisLeft(yScale).tickValues(yTicksForced).tickFormat((v)=>formatAbbrev(v)))
321
- .call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','11px'); });
322
-
323
- // Axis labels (only X; Y-label removed to gain space)
324
- gAxes.append('text')
325
- .attr('class', 'x-axis-label')
326
- .attr('x', innerWidth / 2)
327
- .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 12)))
328
- .attr('fill', 'var(--text-color)')
329
- .attr('text-anchor', 'middle')
330
- .style('font-size', '8px')
331
- .style('opacity', '.5')
332
- .style('letter-spacing', '.5px')
333
- .style('text-transform', 'uppercase')
334
- .style('font-weight', '500')
335
- .text('Steps');
336
-
337
- return { innerWidth, innerHeight, xTicksForced, yTicksForced };
338
- }
339
-
340
- // Generic number abbreviation for ticks (K/M/B) with up to 2 decimals
341
- const formatAbbrev = (value) => {
342
- const num = Number(value);
343
- if (!Number.isFinite(num)) return String(value);
344
- const abs = Math.abs(num);
345
- const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, '');
346
- if (abs >= 1e9) return `${trim2(num / 1e9)}B`;
347
- if (abs >= 1e6) return `${trim2(num / 1e6)}M`;
348
- if (abs >= 1e3) return `${trim2(num / 1e3)}K`;
349
- return trim2(num);
350
- };
351
-
352
- function render(metricData, colorForRun) {
353
- const runs = Object.keys(metricData || {});
354
- const hasAny = runs.some(r => (metricData[r]||[]).length > 0);
355
- if (!hasAny) {
356
- // Show a single empty message and hide the SVG group
357
- gRoot.style('display', 'none');
358
- let msg = body.querySelector('.empty-msg');
359
- if (!msg) {
360
- msg = document.createElement('div');
361
- msg.className = 'empty-msg';
362
- msg.textContent = 'Metric not found in data.';
363
- Object.assign(msg.style, { padding:'10px', fontSize:'12px', color:'var(--muted-color)' });
364
- body.appendChild(msg);
365
- }
366
- return;
367
- }
368
- // Ensure message is removed if previously shown
369
- const msg = body.querySelector('.empty-msg'); if (msg) msg.remove();
370
- gRoot.style('display', null);
371
-
372
- let minStep = Infinity, maxStep = -Infinity, minVal = Infinity, maxVal = -Infinity;
373
- 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); }); });
374
- const isAccuracy = /accuracy/i.test(metricKey);
375
- const axisLabelY = prettyMetricLabel(metricKey);
376
- if (isAccuracy) yScale.domain([0, 1]).nice(); else yScale.domain([minVal, maxVal]).nice();
377
- // Build unique steps and index mapping for equal spacing and snapping
378
- const stepSet = new Set(); runs.forEach(r => (metricData[r]||[]).forEach(v => stepSet.add(v.step)));
379
- const hoverSteps = Array.from(stepSet).sort((a,b)=>a-b);
380
- const stepIndex = new Map(hoverSteps.map((s,i)=>[s,i]));
381
- xScale.domain([0, Math.max(0, hoverSteps.length - 1)]);
382
-
383
- // Update line generator X accessor to use index directly
384
- lineGen.x(d => xScale(stepIndex.get(d.step)));
385
-
386
- const { innerWidth, innerHeight, xTicksForced } = updateLayout(axisLabelY, hoverSteps);
387
-
388
- // Vertical grid lines at each step index (same visibility as horizontal)
389
- gGrid.selectAll('line.vstep')
390
- .data(xTicksForced)
391
- .join(
392
- enter => enter.append('line').attr('class','vstep')
393
- .attr('y1', 0).attr('y2', innerHeight)
394
- .attr('x1', d => xScale(d)).attr('x2', d => xScale(d))
395
- .attr('stroke','var(--grid-color)').attr('stroke-width',1).attr('shape-rendering','crispEdges'),
396
- update => update
397
- .attr('y1', 0).attr('y2', innerHeight)
398
- .attr('x1', d => xScale(d)).attr('x2', d => xScale(d)),
399
- exit => exit.remove()
400
- );
401
-
402
- // Remove stderr correction shapes (no uncertainty area)
403
- gAreas.selectAll('*').remove();
404
-
405
- // Lines
406
- const series = runs.map(r => ({ run: r, color: colorForRun(r), values: (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
407
- const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
408
- 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)
409
- .attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
410
- if (shouldSuppress()) {
411
- paths.attr('stroke', d=>d.color).attr('opacity',0.9).attr('d', d=>lineGen(d.values));
412
- } else {
413
- paths.transition().duration(260).attr('stroke', d=>d.color).attr('opacity',0.9).attr('d', d=>lineGen(d.values));
414
- }
415
- paths.exit().remove();
416
-
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 */ })
424
- .attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value));
425
- if (!shouldSuppress()) {
426
- try { gPoints.selectAll('circle.pt').transition().duration(150).attr('cx', d=>xScale(stepIndex.get(d.step))).attr('cy', d=>yScale(d.value)); } catch(_){}
427
- }
428
- ptsSel.exit().remove();
429
-
430
- // Hover
431
- gHover.selectAll('*').remove();
432
- const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
433
- 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');
434
-
435
- // Expose external hover handlers for cross-chart sync
436
- cell.__showExternalStep = (stepVal) => {
437
- if (stepVal == null) { hoverLine.style('display','none'); return; }
438
- const idx = stepIndex.get(stepVal);
439
- if (idx == null) { hoverLine.style('display','none'); return; }
440
- const xpx = xScale(idx);
441
- hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
442
- };
443
- cell.__clearExternalStep = () => { hoverLine.style('display','none'); };
444
- if (!cell.__syncAttached && host) {
445
- host.addEventListener('trackio-hover-step', (ev) => { const d = ev && ev.detail; if (!d) return; if (cell.__showExternalStep) cell.__showExternalStep(d.step); });
446
- host.addEventListener('trackio-hover-clear', () => { if (cell.__clearExternalStep) cell.__clearExternalStep(); });
447
- cell.__syncAttached = true;
448
- }
449
- 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(_){} }
450
- let html = `<div><strong>step</strong> ${formatAbbrev(nearest)}</div><div><strong>${prettyMetricLabel(metricKey)}</strong></div>`;
451
- const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);
452
- entries.sort((a,b)=> (a.pt.value - b.pt.value));
453
- const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4));
454
- entries.forEach(e => {
455
- 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>`;
456
- });
457
- tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`;
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
-
469
- // Fullscreen: button + modal behavior
470
- const fsBtn = document.createElement('button'); fsBtn.className = 'cell-action cell-action--fullscreen'; fsBtn.type = 'button'; fsBtn.title='Fullscreen'; fsBtn.setAttribute('aria-label','Open fullscreen');
471
- fsBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4 9V4h5v2H6v3H4zm10-5h5v5h-2V6h-3V4zM6 18h3v2H4v-5h2v3zm12-3h2v5h-5v-2h3v-3z"/></svg>';
472
- header.appendChild(fsBtn);
473
-
474
- function ensureOverlay(host){
475
- let overlay = host.querySelector('.d3-trackio__overlay');
476
- if (!overlay) {
477
- overlay = document.createElement('div'); overlay.className='d3-trackio__overlay';
478
- const modal = document.createElement('div'); modal.className='d3-trackio__modal';
479
- const close = document.createElement('button'); close.className='d3-trackio__modal-close'; close.type='button'; close.innerHTML='&#10005;';
480
- overlay.appendChild(modal); overlay.appendChild(close);
481
- host.appendChild(overlay);
482
- overlay.addEventListener('click', (e)=>{ if (e.target === overlay) close.click(); });
483
- close.addEventListener('click', ()=>{
484
- const moving = modal.querySelector('.cell'); if (!moving) { overlay.classList.remove('is-open'); return; }
485
- const placeholder = moving.__placeholder;
486
- if (!placeholder) { overlay.classList.remove('is-open'); return; }
487
- // Global suppression across charts inside this host
488
- host.dataset.suppressTransitions = '1';
489
- // Cancel any in-flight D3 transitions
490
- try { d3.select(host).selectAll('path.run-line').interrupt(); d3.select(host).selectAll('circle.pt').interrupt(); } catch(_){ }
491
- // FLIP: animate from current (modal) to placeholder position
492
- const from = moving.getBoundingClientRect();
493
- const to = placeholder.getBoundingClientRect();
494
- const dx = to.left - from.left; const dy = to.top - from.top;
495
- const sx = Math.max(0.0001, to.width / from.width);
496
- const sy = Math.max(0.0001, to.height / from.height);
497
- moving.style.transformOrigin = 'top left';
498
- moving.style.transition = 'transform 220ms cubic-bezier(.2,.8,.2,1)';
499
- moving.style.transform = `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
500
- const onEnd = () => {
501
- moving.removeEventListener('transitionend', onEnd);
502
- overlay.classList.remove('is-open');
503
- moving.style.transition = '';
504
- moving.style.transform = '';
505
- if (placeholder && placeholder.parentNode) { placeholder.parentNode.insertBefore(moving, placeholder); placeholder.remove(); }
506
- moving.removeAttribute('data-height');
507
- // Recompute layout without any line/point animation
508
- try { const h = host; if (h && h.__rerender) h.__rerender(); } catch {}
509
- requestAnimationFrame(()=>{ delete host.dataset.suppressTransitions; });
510
- };
511
- moving.addEventListener('transitionend', onEnd);
512
- });
513
- window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && overlay.classList.contains('is-open')) { const btn = overlay.querySelector('.d3-trackio__modal-close'); btn && btn.click(); }});
514
- }
515
- return overlay;
516
- }
517
-
518
- fsBtn.addEventListener('click', ()=>{
519
- const hostNode = cell.closest('.d3-trackio'); if (!hostNode) return;
520
- const overlay = ensureOverlay(hostNode); const modal = overlay.querySelector('.d3-trackio__modal');
521
- // If another chart is open, close it first then proceed
522
- const existing = modal.querySelector('.cell');
523
- if (overlay.classList.contains('is-open') && existing && existing !== cell) {
524
- const btn = overlay.querySelector('.d3-trackio__modal-close');
525
- if (btn) { btn.dispatchEvent(new Event('click')); }
526
- const waitEnd = () => {
527
- if (!overlay.classList.contains('is-open')) { fsBtn.click(); return; }
528
- requestAnimationFrame(waitEnd);
529
- };
530
- waitEnd();
531
- return;
532
- }
533
- // Global suppression across charts inside this host
534
- hostNode.dataset.suppressTransitions = '1';
535
- // Cancel any in-flight D3 transitions
536
- try { d3.select(hostNode).selectAll('path.run-line').interrupt(); d3.select(hostNode).selectAll('circle.pt').interrupt(); } catch(_){ }
537
- const before = cell.getBoundingClientRect();
538
- const placeholder = document.createElement('div'); placeholder.style.width = cell.offsetWidth + 'px'; placeholder.style.height = cell.offsetHeight + 'px';
539
- cell.__placeholder = placeholder; cell.parentNode.insertBefore(placeholder, cell);
540
- modal.appendChild(cell);
541
- // Do NOT re-render or change SVG internals; scale via CSS only
542
- const after = cell.getBoundingClientRect();
543
- const dx = before.left - after.left; const dy = before.top - after.top;
544
- const sx = Math.max(0.0001, before.width / after.width);
545
- const sy = Math.max(0.0001, before.height / after.height);
546
- cell.style.transformOrigin = 'top left';
547
- cell.style.transition = 'transform 220ms cubic-bezier(.2,.8,.2,1)';
548
- cell.style.transform = `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
549
- overlay.classList.add('is-open');
550
- requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ cell.style.transform = 'none'; }); });
551
- const onEnd = () => { cell.removeEventListener('transitionend', onEnd); cell.style.transition = ''; cell.style.transform = ''; requestAnimationFrame(()=>{ /* keep suppress until settled or close */ }); };
552
- cell.addEventListener('transitionend', onEnd);
553
- });
554
-
555
- return { metricKey, render };
556
- }
557
-
558
- const bootstrap = () => {
559
- const scriptEl = THIS_SCRIPT;
560
- // Locate host
561
- let host = null;
562
- if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
563
- host = scriptEl.parentElement.querySelector('.d3-trackio');
564
- }
565
- if (!host) {
566
- let sib = scriptEl && scriptEl.previousElementSibling;
567
- while (sib && !(sib.classList && sib.classList.contains('d3-trackio'))) { sib = sib.previousElementSibling; }
568
- host = sib || null;
569
- }
570
- if (!host) { host = document.querySelector('.d3-trackio'); }
571
- if (!host) return;
572
- if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
573
-
574
- // Build global header (legend) and insert ABOVE the grid
575
- const header = document.createElement('div'); header.className = 'd3-trackio__header';
576
- const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Runs</div><div class="items"></div>';
577
- header.appendChild(legend);
578
- const gridNode = host.querySelector('.d3-trackio__grid');
579
- if (gridNode && gridNode.parentNode === host) { host.insertBefore(header, gridNode); } else { host.insertBefore(header, host.firstChild); }
580
-
581
- const cells = Array.from(host.querySelectorAll('.cell'));
582
- if (!cells.length) return;
583
-
584
- const instances = cells.map(cell => initCell(cell));
585
-
586
- // Read HtmlEmbed attributes from closest ancestor carrying them
587
- let mountEl = host;
588
- while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
589
- mountEl = mountEl.parentElement;
590
- }
591
- let providedData = null; let providedConfig = null;
592
- try { const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null; if (attr && attr.trim()) { providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim(); } } catch(_) {}
593
- try { const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; if (cfg && cfg.trim()) { providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; } } catch(_) {}
594
-
595
- const DEFAULT_CSV = '/data/trackio_wandb_demo.csv';
596
- const ensureDataPrefix = (p) => { if (typeof p !== 'string' || !p) return p; return p.includes('/') ? p : `/data/${p}`; };
597
- const normalizeInput = (inp) => Array.isArray(inp) ? inp.map(ensureDataPrefix) : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
598
- const CSV_PATHS = Array.isArray(providedData)
599
- ? normalizeInput(providedData)
600
- : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
601
- DEFAULT_CSV,
602
- './assets/data/formatting_filters.csv',
603
- '../assets/data/formatting_filters.csv',
604
- '../../assets/data/formatting_filters.csv'
605
- ]);
606
- const fetchFirstAvailable = async (paths) => { for (const p of paths) { try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(_){} } throw new Error('CSV not found'); };
607
-
608
- const d3 = window.d3;
609
- (async () => {
610
- try {
611
- // Load one or many CSVs and merge rows
612
- let textAll = '';
613
- if (Array.isArray(CSV_PATHS) && CSV_PATHS.length > 1) {
614
- const texts = await Promise.all(CSV_PATHS.map(p => fetch(p, { cache:'no-cache' }).then(r => r.ok ? r.text() : '').catch(()=>'')));
615
- textAll = texts.filter(Boolean).join('\n');
616
- } else {
617
- textAll = await fetchFirstAvailable(CSV_PATHS);
618
- }
619
- const rows = d3.csvParse(textAll, d => ({
620
- run:(d.run||'').trim(),
621
- step:+d.step,
622
- metric:(d.metric||'').trim(),
623
- value:+d.value,
624
- stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null
625
- }));
626
-
627
- // Filter to target metrics if present in data, with synonym resolution
628
- const metricsInData = Array.from(new Set(rows.map(r => r.metric)));
629
- const lcSet = new Set(metricsInData.map(m => m.toLowerCase()));
630
- const preferIfExists = (cand) => cand.find(c => lcSet.has(String(c).toLowerCase())) || null;
631
- const resolveMetric = (target) => {
632
- // Config override
633
- const override = providedConfig && providedConfig.metricMap && providedConfig.metricMap[target];
634
- if (override && lcSet.has(String(override).toLowerCase())) return metricsInData.find(m => m.toLowerCase() === String(override).toLowerCase());
635
- // Exact
636
- const exact = metricsInData.find(m => m.toLowerCase() === target.toLowerCase()); if (exact) return exact;
637
- // Heuristics by target
638
- const cands = (name) => metricsInData.filter(m => m.toLowerCase().includes(name));
639
- if (target === 'epoch') return preferIfExists(['epoch']);
640
- if (target === 'train_accuracy') return preferIfExists([
641
- 'train_accuracy','training_accuracy','accuracy_train','train_acc','acc_train','train/accuracy','accuracy'
642
- ]) || cands('acc').find(m => /train|trn/i.test(m));
643
- if (target === 'val_accuracy') return preferIfExists([
644
- 'val_accuracy','valid_accuracy','validation_accuracy','val_acc','acc_val','val/accuracy'
645
- ]) || cands('acc').find(m => /val|valid/i.test(m));
646
- if (target === 'train_loss') return preferIfExists([
647
- 'train_loss','training_loss','loss_train','train/loss','loss'
648
- ]) || cands('loss').find(m => /train|trn/i.test(m));
649
- if (target === 'val_loss') return preferIfExists([
650
- 'val_loss','validation_loss','valid_loss','loss_val','val/loss'
651
- ]) || cands('loss').find(m => /val|valid/i.test(m));
652
- return null;
653
- };
654
- const TARGET_TO_DATA = Object.fromEntries(TARGET_METRICS.map(t => [t, resolveMetric(t)]));
655
- const metricsToDraw = TARGET_METRICS.filter(t => !!TARGET_TO_DATA[t]);
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();
702
- metricsToDraw.forEach(tgt => {
703
- const m = TARGET_TO_DATA[tgt];
704
- const map = {}; runList.forEach(r => map[r] = []);
705
- rows.filter(r=>r.metric===m).forEach(r => {
706
- if (!isNaN(r.step) && !isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr });
707
- });
708
- dataByMetric.set(tgt, map);
709
- });
710
-
711
- // Render each cell
712
- instances.forEach(inst => {
713
- const metricMap = dataByMetric.get(inst.metricKey) || {};
714
- inst.render(metricMap, colorForRun);
715
- });
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);
835
- }
836
- })();
837
- };
838
-
839
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
840
- })();
841
- </script>
842
-
843
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/styles/components/_button.css CHANGED
@@ -1,7 +1,7 @@
1
  button, .button {
2
  appearance: none;
3
  background: linear-gradient(15deg, var(--primary-color) 0%, var(--primary-color-hover) 35%);
4
- color: white!important;
5
  border: 1px solid transparent;
6
  border-radius: var(--button-radius);
7
  padding: var(--button-padding-y) var(--button-padding-x);
@@ -15,7 +15,7 @@ button, .button {
15
  /* Icon-only buttons: equal X/Y padding */
16
  button:has(> svg:only-child),
17
  .button:has(> svg:only-child) {
18
- padding: var(--button-icon-padding) !important;
19
  }
20
  button:hover, .button:hover {
21
  filter: brightness(96%);
@@ -32,24 +32,24 @@ button, .button {
32
  }
33
 
34
  /* Ghost/Muted button: subtle outline, primary color text/border */
35
- .button--ghost {
36
  background: transparent !important;
37
  color: var(--primary-color) !important;
38
  border-color: var(--primary-color) !important;
39
  }
40
- .button--ghost:hover {
41
  color: var(--primary-color-hover) !important;
42
  border-color: var(--primary-color-hover) !important;
43
  filter: none;
44
  }
45
 
46
  /* Big button: larger padding and font size */
47
- .button--big {
48
- padding: var(--button-big-padding-y) var(--button-big-padding-x) !important;
49
  font-size: var(--button-big-font-size);
50
  }
51
- .button--big:has(> svg:only-child) {
52
- padding: var(--button-big-icon-padding) !important;
53
  }
54
 
55
  .button-group .button {
 
1
  button, .button {
2
  appearance: none;
3
  background: linear-gradient(15deg, var(--primary-color) 0%, var(--primary-color-hover) 35%);
4
+ color: white;
5
  border: 1px solid transparent;
6
  border-radius: var(--button-radius);
7
  padding: var(--button-padding-y) var(--button-padding-x);
 
15
  /* Icon-only buttons: equal X/Y padding */
16
  button:has(> svg:only-child),
17
  .button:has(> svg:only-child) {
18
+ padding: var(--button-icon-padding);
19
  }
20
  button:hover, .button:hover {
21
  filter: brightness(96%);
 
32
  }
33
 
34
  /* Ghost/Muted button: subtle outline, primary color text/border */
35
+ .button.button--ghost {
36
  background: transparent !important;
37
  color: var(--primary-color) !important;
38
  border-color: var(--primary-color) !important;
39
  }
40
+ .button.button--ghost:hover {
41
  color: var(--primary-color-hover) !important;
42
  border-color: var(--primary-color-hover) !important;
43
  filter: none;
44
  }
45
 
46
  /* Big button: larger padding and font size */
47
+ .button.button--big {
48
+ padding: var(--button-big-padding-y) var(--button-big-padding-x);
49
  font-size: var(--button-big-font-size);
50
  }
51
+ .button.button--big:has(> svg:only-child) {
52
+ padding: var(--button-big-icon-padding);
53
  }
54
 
55
  .button-group .button {