| | |
| |
|
| | export const formatAbbrev = (value) => { |
| | const num = Number(value); |
| | if (!Number.isFinite(num)) return String(value); |
| | const abs = Math.abs(num); |
| | const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, ''); |
| | if (abs >= 1e9) return `${trim2(num / 1e9)}B`; |
| | if (abs >= 1e6) return `${trim2(num / 1e6)}M`; |
| | if (abs >= 1e3) return `${trim2(num / 1e3)}K`; |
| | return trim2(num); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export const formatLogTick = (value, isLogScale = false) => { |
| | if (!isLogScale) return formatAbbrev(value); |
| | |
| | const num = Number(value); |
| | if (!Number.isFinite(num)) return String(value); |
| | |
| | |
| | const log10 = Math.log10(Math.abs(num)); |
| | const isPowerOf10 = Math.abs(log10 % 1) < 0.01; |
| | |
| | if (isPowerOf10) { |
| | |
| | const power = Math.round(log10); |
| | if (power >= 0 && power <= 6) { |
| | |
| | return formatAbbrev(value); |
| | } else { |
| | |
| | return `10^${power}`; |
| | } |
| | } |
| | |
| | |
| | return formatAbbrev(value); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function generateLogTicks(stepValues, minTicks, maxTicks, width, scale) { |
| | if (!stepValues || stepValues.length === 0 || !scale) return { major: [], minor: [] }; |
| | |
| | const minPixelSpacing = 50; |
| | const minorPixelSpacing = 25; |
| | const maxTicksFromWidth = Math.max(4, Math.floor(width / minPixelSpacing)); |
| | const targetMaxTicks = Math.min(maxTicks, maxTicksFromWidth); |
| | |
| | |
| | console.log('🎯 generateLogTicks called:', { |
| | stepCount: stepValues.length, |
| | stepRange: [Math.min(...stepValues), Math.max(...stepValues)], |
| | targetTicks: [minTicks, targetMaxTicks], |
| | width |
| | }); |
| | |
| | const domain = scale.domain(); |
| | const [minVal, maxVal] = domain; |
| | |
| | |
| | const logMin = Math.log10(minVal); |
| | const logMax = Math.log10(maxVal); |
| | const logRange = logMax - logMin; |
| | |
| | |
| | const majorCandidates = new Set(); |
| | const minorCandidates = new Set(); |
| | |
| | |
| | majorCandidates.add(minVal); |
| | majorCandidates.add(maxVal); |
| | |
| | const startPower = Math.floor(logMin); |
| | const endPower = Math.ceil(logMax); |
| | |
| | |
| | for (let power = startPower; power <= endPower; power++) { |
| | const value = Math.pow(10, power); |
| | if (value >= minVal && value <= maxVal) { |
| | majorCandidates.add(value); |
| | } |
| | } |
| | |
| | |
| | if (logRange > 0.7) { |
| | for (let power = startPower; power <= endPower; power++) { |
| | const base = Math.pow(10, power); |
| | [2, 5].forEach(multiplier => { |
| | const value = base * multiplier; |
| | if (value >= minVal && value <= maxVal) { |
| | majorCandidates.add(value); |
| | } |
| | }); |
| | } |
| | } |
| | |
| | |
| | for (let power = startPower; power <= endPower; power++) { |
| | const base = Math.pow(10, power); |
| | |
| | [3, 4, 6, 7, 8, 9].forEach(multiplier => { |
| | const value = base * multiplier; |
| | if (value >= minVal && value <= maxVal && !majorCandidates.has(value)) { |
| | minorCandidates.add(value); |
| | } |
| | }); |
| | } |
| | |
| | |
| | const matchToStepValues = (candidates) => { |
| | return Array.from(candidates).map(candidate => { |
| | let closest = stepValues[0]; |
| | let minRelativeDistance = Math.abs(stepValues[0] - candidate) / Math.max(stepValues[0], candidate); |
| | |
| | stepValues.forEach(step => { |
| | const relativeDistance = Math.abs(step - candidate) / Math.max(step, candidate); |
| | if (relativeDistance < minRelativeDistance) { |
| | minRelativeDistance = relativeDistance; |
| | closest = step; |
| | } |
| | }); |
| | |
| | |
| | const isPowerOf10 = Math.abs(Math.log10(candidate) % 1) < 0.01; |
| | const tolerance = majorCandidates.has(candidate) ? 0.15 : 0.20; |
| | |
| | if (minRelativeDistance < tolerance || isPowerOf10) { |
| | return closest; |
| | } |
| | return null; |
| | }).filter(v => v !== null); |
| | }; |
| | |
| | let majorTicks = Array.from(new Set(matchToStepValues(majorCandidates))).sort((a, b) => a - b); |
| | let minorTicks = Array.from(new Set(matchToStepValues(minorCandidates))).sort((a, b) => a - b); |
| | |
| | |
| | const filteredMajorTicks = []; |
| | majorTicks.forEach(tick => { |
| | if (filteredMajorTicks.length === 0) { |
| | filteredMajorTicks.push(tick); |
| | } else { |
| | const prevTick = filteredMajorTicks[filteredMajorTicks.length - 1]; |
| | const pixelDistance = Math.abs(scale(tick) - scale(prevTick)); |
| | if (pixelDistance >= minPixelSpacing) { |
| | filteredMajorTicks.push(tick); |
| | } |
| | } |
| | }); |
| | |
| | |
| | const filteredMinorTicks = []; |
| | minorTicks.forEach(tick => { |
| | |
| | const tooCloseToMajor = filteredMajorTicks.some(majorTick => { |
| | const distance = Math.abs(scale(tick) - scale(majorTick)); |
| | return distance < minorPixelSpacing; |
| | }); |
| | |
| | if (!tooCloseToMajor) { |
| | |
| | if (filteredMinorTicks.length === 0) { |
| | filteredMinorTicks.push(tick); |
| | } else { |
| | const prevTick = filteredMinorTicks[filteredMinorTicks.length - 1]; |
| | const pixelDistance = Math.abs(scale(tick) - scale(prevTick)); |
| | if (pixelDistance >= minorPixelSpacing) { |
| | filteredMinorTicks.push(tick); |
| | } |
| | } |
| | } |
| | }); |
| | |
| | const result = { |
| | major: filteredMajorTicks.length >= 2 ? filteredMajorTicks : [minVal, maxVal], |
| | minor: filteredMinorTicks |
| | }; |
| | |
| | |
| | console.log('🎯 generateLogTicks result:', { |
| | logRange: logRange.toFixed(2), |
| | majorCount: result.major.length, |
| | minorCount: result.minor.length, |
| | majorTicks: result.major, |
| | minorTicks: result.minor |
| | }); |
| | |
| | return result; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function generateSmartTicks(steps, minTicks, maxTicks, width) { |
| | if (!steps || steps.length === 0) return []; |
| | |
| | const totalSteps = steps.length; |
| | const minPixelSpacing = 75; |
| | const maxTicksFromWidth = Math.max(3, Math.floor(width / minPixelSpacing)); |
| | |
| | |
| | const getMinStepDifference = (totalSteps, width) => { |
| | const pixelsPerStep = width / (totalSteps - 1); |
| | return Math.ceil(minPixelSpacing / pixelsPerStep); |
| | }; |
| | |
| | const minStepDiff = getMinStepDifference(totalSteps, width); |
| | const maxPossibleTicks = Math.floor((totalSteps - 1) / minStepDiff) + 1; |
| | |
| | |
| | const targetMinTicks = Math.min(Math.max(minTicks, 5), maxPossibleTicks, totalSteps); |
| | const targetMaxTicks = Math.min(maxTicks, maxTicksFromWidth, maxPossibleTicks); |
| | |
| | |
| | if (targetMinTicks <= 2 || totalSteps <= 2) { |
| | return [0, totalSteps - 1]; |
| | } |
| | |
| | |
| | const hasValidSpacing = (ticks) => { |
| | for (let i = 1; i < ticks.length; i++) { |
| | if (ticks[i] - ticks[i-1] < minStepDiff) return false; |
| | } |
| | return true; |
| | }; |
| | |
| | |
| | const candidateIntervals = []; |
| | const niceIntervals = [1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000]; |
| | |
| | for (const interval of niceIntervals) { |
| | if (interval >= totalSteps) continue; |
| | |
| | const candidateTicks = [0]; |
| | const firstStepValue = steps[0]; |
| | const lastStepValue = steps[totalSteps - 1]; |
| | const firstNiceValue = Math.ceil(firstStepValue / interval) * interval; |
| | |
| | |
| | for (let niceValue = firstNiceValue; niceValue < lastStepValue; niceValue += interval) { |
| | let closestIndex = 0; |
| | let minDist = Infinity; |
| | for (let i = 0; i < steps.length; i++) { |
| | const dist = Math.abs(steps[i] - niceValue); |
| | if (dist < minDist) { |
| | minDist = dist; |
| | closestIndex = i; |
| | } |
| | } |
| | |
| | |
| | if (minDist <= interval * 0.15 && closestIndex > 0 && closestIndex < totalSteps - 1) { |
| | candidateTicks.push(closestIndex); |
| | } |
| | } |
| | |
| | candidateTicks.push(totalSteps - 1); |
| | const uniqueTicks = [...new Set(candidateTicks)].sort((a,b) => a-b); |
| | |
| | if (hasValidSpacing(uniqueTicks) && uniqueTicks.length >= 3) { |
| | candidateIntervals.push({ |
| | interval: interval, |
| | ticks: uniqueTicks, |
| | count: uniqueTicks.length, |
| | niceness: (interval <= 10 ? 100 : (interval <= 50 ? 50 : (interval <= 100 ? 25 : 10))) |
| | }); |
| | } |
| | } |
| | |
| | |
| | if (candidateIntervals.length === 0 || candidateIntervals.every(c => c.count < targetMinTicks)) { |
| | |
| | for (let targetCount = Math.min(targetMinTicks, maxPossibleTicks); targetCount >= 3; targetCount--) { |
| | |
| | const evenSpacing = Math.floor((totalSteps - 1) / (targetCount - 1)); |
| | const evenTicks = []; |
| | for (let i = 0; i < targetCount - 1; i++) { |
| | evenTicks.push(i * evenSpacing); |
| | } |
| | evenTicks.push(totalSteps - 1); |
| | |
| | if (hasValidSpacing(evenTicks)) { |
| | candidateIntervals.push({ |
| | interval: evenSpacing, |
| | ticks: evenTicks, |
| | count: evenTicks.length, |
| | niceness: 5 |
| | }); |
| | break; |
| | } |
| | |
| | |
| | if (targetCount <= maxPossibleTicks) { |
| | const optimalSpacing = Math.max(minStepDiff, Math.floor((totalSteps - 1) / (targetCount - 1))); |
| | const spacedTicks = [0]; |
| | let currentPos = 0; |
| | |
| | for (let i = 1; i < targetCount - 1; i++) { |
| | currentPos += optimalSpacing; |
| | if (currentPos < totalSteps - 1) { |
| | spacedTicks.push(Math.min(currentPos, totalSteps - 1 - minStepDiff)); |
| | } |
| | } |
| | spacedTicks.push(totalSteps - 1); |
| | |
| | const uniqueSpacedTicks = [...new Set(spacedTicks)].sort((a,b) => a-b); |
| | if (hasValidSpacing(uniqueSpacedTicks) && uniqueSpacedTicks.length >= targetCount - 1) { |
| | candidateIntervals.push({ |
| | interval: optimalSpacing, |
| | ticks: uniqueSpacedTicks, |
| | count: uniqueSpacedTicks.length, |
| | niceness: 3 |
| | }); |
| | break; |
| | } |
| | } |
| | } |
| | } |
| | |
| | |
| | if (candidateIntervals.length === 0) { |
| | const middle = Math.floor(totalSteps / 2); |
| | if (middle !== 0 && middle !== totalSteps - 1 && |
| | middle - 0 >= minStepDiff && totalSteps - 1 - middle >= minStepDiff) { |
| | return [0, middle, totalSteps - 1]; |
| | } |
| | return [0, totalSteps - 1]; |
| | } |
| | |
| | |
| | candidateIntervals.sort((a, b) => { |
| | const aHasEnoughTicks = a.count >= targetMinTicks; |
| | const bHasEnoughTicks = b.count >= targetMinTicks; |
| | |
| | |
| | if (aHasEnoughTicks !== bHasEnoughTicks) { |
| | return bHasEnoughTicks ? 1 : -1; |
| | } |
| | |
| | |
| | if (a.niceness !== b.niceness) { |
| | return b.niceness - a.niceness; |
| | } |
| | |
| | |
| | return b.count - a.count; |
| | }); |
| | |
| | return candidateIntervals[0].ticks; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function smoothData(data, windowSize = 5) { |
| | if (!data || data.length === 0) return data; |
| | if (data.length < windowSize) return data; |
| | |
| | const smoothed = []; |
| | const halfWindow = Math.floor(windowSize / 2); |
| | |
| | for (let i = 0; i < data.length; i++) { |
| | const start = Math.max(0, i - halfWindow); |
| | const end = Math.min(data.length - 1, i + halfWindow); |
| | |
| | let sum = 0; |
| | let count = 0; |
| | |
| | |
| | for (let j = start; j <= end; j++) { |
| | const distance = Math.abs(j - i); |
| | const weight = distance === 0 ? 2 : (distance === 1 ? 1.5 : 1); |
| | sum += data[j].value * weight; |
| | count += weight; |
| | } |
| | |
| | smoothed.push({ |
| | step: data[i].step, |
| | value: sum / count |
| | }); |
| | } |
| | |
| | return smoothed; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function smoothMetricData(metricData, windowSize = 5) { |
| | if (!metricData) return metricData; |
| | |
| | const smoothedData = {}; |
| | Object.keys(metricData).forEach(runName => { |
| | smoothedData[runName] = smoothData(metricData[runName], windowSize); |
| | }); |
| | |
| | return smoothedData; |
| | } |
| |
|