| <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> |
|
|