|
|
<script> |
|
|
import { onMount, onDestroy } from 'svelte'; |
|
|
import { SVGManager } from './core/svg-manager.js'; |
|
|
import { GridRenderer } from './core/grid-renderer.js'; |
|
|
import { PathRenderer } from './core/path-renderer.js'; |
|
|
import { InteractionManager } from './core/interaction-manager.js'; |
|
|
import { ChartTransforms } from './utils/chart-transforms.js'; |
|
|
import { trackioSampler } from '../core/adaptive-sampler.js'; |
|
|
|
|
|
|
|
|
export let metricData = {}; |
|
|
export let rawMetricData = {}; |
|
|
export let colorForRun = (name) => '#999'; |
|
|
export let variant = 'classic'; |
|
|
export let logScaleX = false; |
|
|
export let smoothing = false; |
|
|
export let normalizeLoss = true; |
|
|
export let metricKey = ''; |
|
|
export let titleText = ''; |
|
|
export let hostEl = null; |
|
|
export let width = 800; |
|
|
export let height = 150; |
|
|
export let margin = { top: 10, right: 12, bottom: 46, left: 44 }; |
|
|
export let onHover = null; |
|
|
export let onLeave = null; |
|
|
|
|
|
|
|
|
let container; |
|
|
let svgManager; |
|
|
let gridRenderer; |
|
|
let pathRenderer; |
|
|
let interactionManager; |
|
|
let cleanup; |
|
|
|
|
|
|
|
|
let sampledData = {}; |
|
|
let samplingInfo = {}; |
|
|
let needsSampling = false; |
|
|
|
|
|
|
|
|
$: innerHeight = height - margin.top - margin.bottom; |
|
|
|
|
|
|
|
|
$: { |
|
|
if (container && svgManager) { |
|
|
// List all dependencies to trigger render when any change |
|
|
void metricData; |
|
|
void metricKey; |
|
|
void variant; |
|
|
void logScaleX; |
|
|
void normalizeLoss; |
|
|
void smoothing; |
|
|
render(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeManagers() { |
|
|
if (!container) return; |
|
|
|
|
|
// Create SVG manager with configuration |
|
|
svgManager = new SVGManager(container, { width, height, margin }); |
|
|
svgManager.ensureSvg(); |
|
|
svgManager.initializeScales(logScaleX); |
|
|
|
|
|
|
|
|
gridRenderer = new GridRenderer(svgManager); |
|
|
pathRenderer = new PathRenderer(svgManager); |
|
|
interactionManager = new InteractionManager(svgManager, pathRenderer); |
|
|
|
|
|
console.log('π Chart managers initialized'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function applySampling() { |
|
|
// Check if any run has more than 400 points |
|
|
const runSizes = Object.keys(metricData).map(run => (metricData[run] || []).length); |
|
|
const maxSize = Math.max(0, ...runSizes); |
|
|
needsSampling = maxSize > 400; |
|
|
|
|
|
if (needsSampling) { |
|
|
console.log(`π― Large dataset detected (${maxSize} points), applying adaptive sampling`); |
|
|
const result = trackioSampler.sampleMetricData(metricData, 'smart'); |
|
|
sampledData = result.sampledData; |
|
|
samplingInfo = result.samplingInfo; |
|
|
|
|
|
// Log sampling stats |
|
|
Object.keys(samplingInfo).forEach(run => { |
|
|
const info = samplingInfo[run]; |
|
|
console.log(`π ${run}: ${info.originalLength} β ${info.sampledLength} points (${(info.compressionRatio * 100).toFixed(1)}% retained)`); |
|
|
}); |
|
|
} else { |
|
|
sampledData = metricData; |
|
|
samplingInfo = {}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function render() { |
|
|
if (!svgManager) return; |
|
|
|
|
|
// Apply sampling if needed |
|
|
applySampling(); |
|
|
|
|
|
// Use sampled data for rendering |
|
|
const dataToRender = needsSampling ? sampledData : metricData; |
|
|
|
|
|
// Validate and clean data |
|
|
const cleanedData = ChartTransforms.validateData(dataToRender); |
|
|
const processedData = ChartTransforms.processMetricData(cleanedData, metricKey, normalizeLoss); |
|
|
|
|
|
if (!processedData.hasData) { |
|
|
const { root } = svgManager.getGroups(); |
|
|
root.style('display', 'none'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const { root } = svgManager.getGroups(); |
|
|
root.style('display', null); |
|
|
|
|
|
|
|
|
svgManager.initializeScales(logScaleX); |
|
|
|
|
|
|
|
|
const { stepIndex } = ChartTransforms.setupScales(svgManager, processedData, logScaleX); |
|
|
const normalizeY = ChartTransforms.createNormalizeFunction(processedData, normalizeLoss); |
|
|
|
|
|
|
|
|
const { line: lineGen, y: yScale } = svgManager.getScales(); |
|
|
lineGen.y(d => yScale(normalizeY(d.value))); |
|
|
|
|
|
|
|
|
const { innerWidth, xTicksForced, yTicksForced } = svgManager.updateLayout(processedData.hoverSteps, logScaleX); |
|
|
|
|
|
|
|
|
gridRenderer.renderGrid(xTicksForced, yTicksForced, processedData.hoverSteps, variant); |
|
|
|
|
|
|
|
|
pathRenderer.renderSeries( |
|
|
processedData.runs, |
|
|
cleanedData, |
|
|
rawMetricData, |
|
|
colorForRun, |
|
|
smoothing, |
|
|
logScaleX, |
|
|
stepIndex, |
|
|
normalizeY |
|
|
); |
|
|
|
|
|
|
|
|
interactionManager.setupHoverInteractions( |
|
|
processedData.hoverSteps, |
|
|
stepIndex, |
|
|
processedData.runs.map(r => ({ |
|
|
run: r, |
|
|
color: colorForRun(r), |
|
|
values: (cleanedData[r] || []).slice().sort((a, b) => a.step - b.step) |
|
|
})), |
|
|
normalizeY, |
|
|
processedData.isAccuracy, |
|
|
innerWidth, |
|
|
logScaleX, |
|
|
onHover, |
|
|
onLeave |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function showHoverLine(step) { |
|
|
if (!interactionManager) return; |
|
|
|
|
|
// Use sampled data for interactions as well |
|
|
const dataToRender = needsSampling ? sampledData : metricData; |
|
|
const cleanedData = ChartTransforms.validateData(dataToRender); |
|
|
const processedData = ChartTransforms.processMetricData(cleanedData, metricKey, normalizeLoss); |
|
|
const { stepIndex } = ChartTransforms.setupScales(svgManager, processedData, logScaleX); |
|
|
|
|
|
interactionManager.showHoverLine(step, processedData.hoverSteps, stepIndex, logScaleX); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function hideHoverLine() { |
|
|
if (interactionManager) { |
|
|
interactionManager.hideHoverLine(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMount(() => { |
|
|
initializeManagers(); |
|
|
render(); |
|
|
|
|
|
// Debounced resize handling for better mobile performance |
|
|
let resizeTimeout; |
|
|
const debouncedRender = () => { |
|
|
if (resizeTimeout) clearTimeout(resizeTimeout); |
|
|
resizeTimeout = setTimeout(() => { |
|
|
render(); |
|
|
}, 100); |
|
|
}; |
|
|
|
|
|
const ro = window.ResizeObserver ? new ResizeObserver(debouncedRender) : null; |
|
|
if (ro && container) ro.observe(container); |
|
|
|
|
|
|
|
|
const handleOrientationChange = () => { |
|
|
setTimeout(() => { |
|
|
render(); |
|
|
}, 300); |
|
|
}; |
|
|
|
|
|
window.addEventListener('orientationchange', handleOrientationChange); |
|
|
window.addEventListener('resize', debouncedRender); |
|
|
|
|
|
cleanup = () => { |
|
|
if (ro) ro.disconnect(); |
|
|
if (resizeTimeout) clearTimeout(resizeTimeout); |
|
|
window.removeEventListener('orientationchange', handleOrientationChange); |
|
|
window.removeEventListener('resize', debouncedRender); |
|
|
if (svgManager) svgManager.destroy(); |
|
|
if (interactionManager) interactionManager.destroy(); |
|
|
}; |
|
|
}); |
|
|
|
|
|
onDestroy(() => { |
|
|
cleanup && cleanup(); |
|
|
}); |
|
|
</script> |
|
|
|
|
|
<div bind:this={container} style="width: 100%; height: 100%; min-width: 200px; overflow: hidden;"></div> |
|
|
|