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