| | |
| |
|
| | |
| | |
| | |
| | export class ChartTransforms { |
| | |
| | |
| | |
| | |
| | static processMetricData(metricData, metricKey, normalizeLoss) { |
| | const runs = Object.keys(metricData || {}); |
| | const hasAny = runs.some(r => (metricData[r] || []).length > 0); |
| | |
| | if (!hasAny) { |
| | return { |
| | runs: [], |
| | hasData: false, |
| | minStep: 0, |
| | maxStep: 0, |
| | minVal: 0, |
| | maxVal: 1, |
| | yDomain: [0, 1], |
| | stepSet: new Set(), |
| | hoverSteps: [] |
| | }; |
| | } |
| |
|
| | |
| | let minStep = Infinity, maxStep = -Infinity, minVal = Infinity, maxVal = -Infinity; |
| | 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); |
| | }); |
| | }); |
| | |
| | |
| | const isAccuracy = /accuracy/i.test(metricKey); |
| | const isLoss = /loss/i.test(metricKey); |
| | let yDomain; |
| | |
| | if (isAccuracy) { |
| | yDomain = [0, 1]; |
| | } else if (isLoss && normalizeLoss) { |
| | yDomain = [0, 1]; |
| | } else { |
| | yDomain = [minVal, maxVal]; |
| | } |
| | |
| | |
| | const stepSet = new Set(); |
| | runs.forEach(r => (metricData[r] || []).forEach(v => stepSet.add(v.step))); |
| | const hoverSteps = Array.from(stepSet).sort((a, b) => a - b); |
| | |
| | return { |
| | runs, |
| | hasData: true, |
| | minStep, |
| | maxStep, |
| | minVal, |
| | maxVal, |
| | yDomain, |
| | stepSet, |
| | hoverSteps, |
| | isAccuracy, |
| | isLoss |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | static setupScales(svgManager, processedData, logScaleX) { |
| | const { hoverSteps, yDomain } = processedData; |
| | const { x: xScale, y: yScale, line: lineGen } = svgManager.getScales(); |
| | |
| | |
| | yScale.domain(yDomain).nice(); |
| | |
| | let stepIndex = null; |
| | |
| | if (logScaleX) { |
| | const minStep = Math.max(1, Math.min(...hoverSteps)); |
| | const maxStep = Math.max(...hoverSteps); |
| | xScale.domain([minStep, maxStep]); |
| | lineGen.x(d => xScale(d.step)); |
| | } else { |
| | stepIndex = new Map(hoverSteps.map((s, i) => [s, i])); |
| | xScale.domain([0, Math.max(0, hoverSteps.length - 1)]); |
| | lineGen.x(d => xScale(stepIndex.get(d.step))); |
| | } |
| | |
| | return { stepIndex }; |
| | } |
| |
|
| | |
| | |
| | |
| | static createNormalizeFunction(processedData, normalizeLoss) { |
| | const { isLoss, minVal, maxVal } = processedData; |
| | |
| | return (v) => { |
| | if (isLoss && normalizeLoss) { |
| | return ((maxVal > minVal) ? (v - minVal) / (maxVal - minVal) : 0); |
| | } |
| | return v; |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | static validateData(metricData) { |
| | const cleanedData = {}; |
| | |
| | Object.keys(metricData || {}).forEach(run => { |
| | const values = metricData[run] || []; |
| | cleanedData[run] = values.filter(pt => |
| | pt && |
| | typeof pt.step === 'number' && |
| | typeof pt.value === 'number' && |
| | Number.isFinite(pt.step) && |
| | Number.isFinite(pt.value) |
| | ); |
| | }); |
| | |
| | return cleanedData; |
| | } |
| |
|
| | |
| | |
| | |
| | static calculateOptimalDimensions(dataCount, containerWidth) { |
| | |
| | const minHeight = 120; |
| | const maxHeight = 300; |
| | const baseHeight = 150; |
| | |
| | |
| | const heightMultiplier = Math.min(1.5, 1 + (dataCount / 1000) * 0.5); |
| | const suggestedHeight = Math.min(maxHeight, Math.max(minHeight, baseHeight * heightMultiplier)); |
| | |
| | return { |
| | width: containerWidth || 800, |
| | height: suggestedHeight |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | static prepareHoverSteps(processedData, logScaleX) { |
| | const { hoverSteps } = processedData; |
| | |
| | if (!hoverSteps.length) return { hoverSteps: [], stepIndex: null }; |
| | |
| | let stepIndex = null; |
| | |
| | if (!logScaleX) { |
| | stepIndex = new Map(hoverSteps.map((s, i) => [s, i])); |
| | } |
| | |
| | return { hoverSteps, stepIndex }; |
| | } |
| | } |
| |
|