CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
chart-utils.js424 linesDownload Raw Back to core
1// Chart utilities for axis formatting and tick generation2 3export const formatAbbrev = (value) => {4  const num = Number(value);5  if (!Number.isFinite(num)) return String(value);6  const abs = Math.abs(num);7  const trim2 = (n) => Number(n).toFixed(2).replace(/\.?0+$/, '');8  if (abs >= 1e9) return `${trim2(num / 1e9)}B`;9  if (abs >= 1e6) return `${trim2(num / 1e6)}M`;10  if (abs >= 1e3) return `${trim2(num / 1e3)}K`;11  return trim2(num);12};13 14/**15 * Enhanced formatting for logarithmic scale ticks16 * @param {number} value - The tick value17 * @param {boolean} isLogScale - Whether this is for a log scale18 * @returns {string} Formatted tick label19 */20export const formatLogTick = (value, isLogScale = false) => {21  if (!isLogScale) return formatAbbrev(value);22  23  const num = Number(value);24  if (!Number.isFinite(num)) return String(value);25  26  // Check if it's a power of 1027  const log10 = Math.log10(Math.abs(num));28  const isPowerOf10 = Math.abs(log10 % 1) < 0.01;29  30  if (isPowerOf10) {31    // Format powers of 10 more prominently32    const power = Math.round(log10);33    if (power >= 0 && power <= 6) {34      // For small powers, show the actual number35      return formatAbbrev(value);36    } else {37      // For very large/small powers, use scientific notation38      return `10^${power}`;39    }40  }41  42  // For non-powers of 10, use regular formatting43  return formatAbbrev(value);44};45 46/**47 * Generates optimized tick positions for logarithmic scales48 * @param {Array} stepValues - Array of actual step values (not indices)49 * @param {number} minTicks - Minimum number of ticks desired50 * @param {number} maxTicks - Maximum number of ticks allowed51 * @param {number} width - Chart width in pixels52 * @param {Function} scale - D3 log scale function53 * @returns {Object} Object with major and minor tick positions54 */55export function generateLogTicks(stepValues, minTicks, maxTicks, width, scale) {56  if (!stepValues || stepValues.length === 0 || !scale) return { major: [], minor: [] };57  58  const minPixelSpacing = 50; // Reduced for better density59  const minorPixelSpacing = 25; // Spacing for minor ticks60  const maxTicksFromWidth = Math.max(4, Math.floor(width / minPixelSpacing));61  const targetMaxTicks = Math.min(maxTicks, maxTicksFromWidth);62  63  // Debug logging64  console.log('๐ŸŽฏ generateLogTicks called:', {65    stepCount: stepValues.length,66    stepRange: [Math.min(...stepValues), Math.max(...stepValues)],67    targetTicks: [minTicks, targetMaxTicks],68    width69  });70  71  const domain = scale.domain();72  const [minVal, maxVal] = domain;73  74  // Calculate the range in log space75  const logMin = Math.log10(minVal);76  const logMax = Math.log10(maxVal);77  const logRange = logMax - logMin;78  79  // Generate major ticks (powers of 10)80  const majorCandidates = new Set();81  const minorCandidates = new Set();82  83  // Always add domain boundaries84  majorCandidates.add(minVal);85  majorCandidates.add(maxVal);86  87  const startPower = Math.floor(logMin);88  const endPower = Math.ceil(logMax);89  90  // Major ticks: powers of 1091  for (let power = startPower; power <= endPower; power++) {92    const value = Math.pow(10, power);93    if (value >= minVal && value <= maxVal) {94      majorCandidates.add(value);95    }96  }97  98  // If we have space, add more major ticks (2x, 5x)99  if (logRange > 0.7) {100    for (let power = startPower; power <= endPower; power++) {101      const base = Math.pow(10, power);102      [2, 5].forEach(multiplier => {103        const value = base * multiplier;104        if (value >= minVal && value <= maxVal) {105          majorCandidates.add(value);106        }107      });108    }109  }110  111  // Minor ticks: intermediate values to show log progression112  for (let power = startPower; power <= endPower; power++) {113    const base = Math.pow(10, power);114    // Add 3x, 4x, 6x, 7x, 8x, 9x for visual density115    [3, 4, 6, 7, 8, 9].forEach(multiplier => {116      const value = base * multiplier;117      if (value >= minVal && value <= maxVal && !majorCandidates.has(value)) {118        minorCandidates.add(value);119      }120    });121  }122  123  // Match candidates to actual step values124  const matchToStepValues = (candidates) => {125    return Array.from(candidates).map(candidate => {126      let closest = stepValues[0];127      let minRelativeDistance = Math.abs(stepValues[0] - candidate) / Math.max(stepValues[0], candidate);128      129      stepValues.forEach(step => {130        const relativeDistance = Math.abs(step - candidate) / Math.max(step, candidate);131        if (relativeDistance < minRelativeDistance) {132          minRelativeDistance = relativeDistance;133          closest = step;134        }135      });136      137      // Only include if reasonable match (20% tolerance for minor, 15% for major)138      const isPowerOf10 = Math.abs(Math.log10(candidate) % 1) < 0.01;139      const tolerance = majorCandidates.has(candidate) ? 0.15 : 0.20;140      141      if (minRelativeDistance < tolerance || isPowerOf10) {142        return closest;143      }144      return null;145    }).filter(v => v !== null);146  };147  148  let majorTicks = Array.from(new Set(matchToStepValues(majorCandidates))).sort((a, b) => a - b);149  let minorTicks = Array.from(new Set(matchToStepValues(minorCandidates))).sort((a, b) => a - b);150  151  // Filter major ticks by pixel spacing152  const filteredMajorTicks = [];153  majorTicks.forEach(tick => {154    if (filteredMajorTicks.length === 0) {155      filteredMajorTicks.push(tick);156    } else {157      const prevTick = filteredMajorTicks[filteredMajorTicks.length - 1];158      const pixelDistance = Math.abs(scale(tick) - scale(prevTick));159      if (pixelDistance >= minPixelSpacing) {160        filteredMajorTicks.push(tick);161      }162    }163  });164  165  // Filter minor ticks by pixel spacing and ensure they don't conflict with major ticks166  const filteredMinorTicks = [];167  minorTicks.forEach(tick => {168    // Skip if too close to any major tick169    const tooCloseToMajor = filteredMajorTicks.some(majorTick => {170      const distance = Math.abs(scale(tick) - scale(majorTick));171      return distance < minorPixelSpacing;172    });173    174    if (!tooCloseToMajor) {175      // Check spacing with previous minor tick176      if (filteredMinorTicks.length === 0) {177        filteredMinorTicks.push(tick);178      } else {179        const prevTick = filteredMinorTicks[filteredMinorTicks.length - 1];180        const pixelDistance = Math.abs(scale(tick) - scale(prevTick));181        if (pixelDistance >= minorPixelSpacing) {182          filteredMinorTicks.push(tick);183        }184      }185    }186  });187  188  const result = {189    major: filteredMajorTicks.length >= 2 ? filteredMajorTicks : [minVal, maxVal],190    minor: filteredMinorTicks191  };192  193  // Debug logging194  console.log('๐ŸŽฏ generateLogTicks result:', {195    logRange: logRange.toFixed(2),196    majorCount: result.major.length,197    minorCount: result.minor.length,198    majorTicks: result.major,199    minorTicks: result.minor200  });201  202  return result;203}204 205/**206 * Generates intelligent tick positions for X-axis with nice intervals207 * @param {Array} steps - Array of step values (e.g., [1, 2, 3, ..., 100])208 * @param {number} minTicks - Minimum number of ticks desired209 * @param {number} maxTicks - Maximum number of ticks allowed210 * @param {number} width - Chart width in pixels211 * @returns {Array} Array of step indices for tick positions212 */213export function generateSmartTicks(steps, minTicks, maxTicks, width) {214  if (!steps || steps.length === 0) return [];215  216  const totalSteps = steps.length;217  const minPixelSpacing = 75; // Slightly reduced minimum spacing to allow more ticks218  const maxTicksFromWidth = Math.max(3, Math.floor(width / minPixelSpacing));219  220  // Function to check if ticks would be too close (minimum step difference)221  const getMinStepDifference = (totalSteps, width) => {222    const pixelsPerStep = width / (totalSteps - 1);223    return Math.ceil(minPixelSpacing / pixelsPerStep);224  };225  226  const minStepDiff = getMinStepDifference(totalSteps, width);227  const maxPossibleTicks = Math.floor((totalSteps - 1) / minStepDiff) + 1;228  229  // Ensure we aim for at least 5 ticks if space permits230  const targetMinTicks = Math.min(Math.max(minTicks, 5), maxPossibleTicks, totalSteps);231  const targetMaxTicks = Math.min(maxTicks, maxTicksFromWidth, maxPossibleTicks);232  233  // Start with first and last234  if (targetMinTicks <= 2 || totalSteps <= 2) {235    return [0, totalSteps - 1];236  }237  238  // Helper to validate spacing239  const hasValidSpacing = (ticks) => {240    for (let i = 1; i < ticks.length; i++) {241      if (ticks[i] - ticks[i-1] < minStepDiff) return false;242    }243    return true;244  };245  246  // Try nice intervals first247  const candidateIntervals = [];248  const niceIntervals = [1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000];249  250  for (const interval of niceIntervals) {251    if (interval >= totalSteps) continue;252    253    const candidateTicks = [0];254    const firstStepValue = steps[0];255    const lastStepValue = steps[totalSteps - 1];256    const firstNiceValue = Math.ceil(firstStepValue / interval) * interval;257    258    // Add ticks for nice step values259    for (let niceValue = firstNiceValue; niceValue < lastStepValue; niceValue += interval) {260      let closestIndex = 0;261      let minDist = Infinity;262      for (let i = 0; i < steps.length; i++) {263        const dist = Math.abs(steps[i] - niceValue);264        if (dist < minDist) {265          minDist = dist;266          closestIndex = i;267        }268      }269      270      // Be more permissive with matching (15% instead of 10%)271      if (minDist <= interval * 0.15 && closestIndex > 0 && closestIndex < totalSteps - 1) {272        candidateTicks.push(closestIndex);273      }274    }275    276    candidateTicks.push(totalSteps - 1);277    const uniqueTicks = [...new Set(candidateTicks)].sort((a,b) => a-b);278    279    if (hasValidSpacing(uniqueTicks) && uniqueTicks.length >= 3) {280      candidateIntervals.push({281        interval: interval,282        ticks: uniqueTicks,283        count: uniqueTicks.length,284        niceness: (interval <= 10 ? 100 : (interval <= 50 ? 50 : (interval <= 100 ? 25 : 10)))285      });286    }287  }288  289  // Force generation of ticks if we don't have enough nice ones290  if (candidateIntervals.length === 0 || candidateIntervals.every(c => c.count < targetMinTicks)) {291    // Try multiple approaches to get targetMinTicks292    for (let targetCount = Math.min(targetMinTicks, maxPossibleTicks); targetCount >= 3; targetCount--) {293      // Approach 1: Even distribution294      const evenSpacing = Math.floor((totalSteps - 1) / (targetCount - 1));295      const evenTicks = [];296      for (let i = 0; i < targetCount - 1; i++) {297        evenTicks.push(i * evenSpacing);298      }299      evenTicks.push(totalSteps - 1);300      301      if (hasValidSpacing(evenTicks)) {302        candidateIntervals.push({303          interval: evenSpacing,304          ticks: evenTicks,305          count: evenTicks.length,306          niceness: 5 // Medium priority307        });308        break; // Found a good solution309      }310      311      // Approach 2: Try to fit exactly targetCount ticks with optimal spacing312      if (targetCount <= maxPossibleTicks) {313        const optimalSpacing = Math.max(minStepDiff, Math.floor((totalSteps - 1) / (targetCount - 1)));314        const spacedTicks = [0];315        let currentPos = 0;316        317        for (let i = 1; i < targetCount - 1; i++) {318          currentPos += optimalSpacing;319          if (currentPos < totalSteps - 1) {320            spacedTicks.push(Math.min(currentPos, totalSteps - 1 - minStepDiff));321          }322        }323        spacedTicks.push(totalSteps - 1);324        325        const uniqueSpacedTicks = [...new Set(spacedTicks)].sort((a,b) => a-b);326        if (hasValidSpacing(uniqueSpacedTicks) && uniqueSpacedTicks.length >= targetCount - 1) {327          candidateIntervals.push({328            interval: optimalSpacing,329            ticks: uniqueSpacedTicks,330            count: uniqueSpacedTicks.length,331            niceness: 3 // Lower priority than nice intervals332          });333          break;334        }335      }336    }337  }338  339  // Absolute fallback340  if (candidateIntervals.length === 0) {341    const middle = Math.floor(totalSteps / 2);342    if (middle !== 0 && middle !== totalSteps - 1 && 343        middle - 0 >= minStepDiff && totalSteps - 1 - middle >= minStepDiff) {344      return [0, middle, totalSteps - 1];345    }346    return [0, totalSteps - 1];347  }348  349  // Sort: prioritize having enough ticks, then niceness350  candidateIntervals.sort((a, b) => {351    const aHasEnoughTicks = a.count >= targetMinTicks;352    const bHasEnoughTicks = b.count >= targetMinTicks;353    354    // First: prefer solutions with enough ticks355    if (aHasEnoughTicks !== bHasEnoughTicks) {356      return bHasEnoughTicks ? 1 : -1;357    }358    359    // Second: prefer nicer intervals360    if (a.niceness !== b.niceness) {361      return b.niceness - a.niceness;362    }363    364    // Third: prefer more ticks if both are nice365    return b.count - a.count;366  });367  368  return candidateIntervals[0].ticks;369}370 371/**372 * Applies smoothing to data series using moving average373 * @param {Array} data - Array of {step, value} objects374 * @param {number} windowSize - Size of the smoothing window (default: 5)375 * @returns {Array} Smoothed data series376 */377export function smoothData(data, windowSize = 5) {378  if (!data || data.length === 0) return data;379  if (data.length < windowSize) return data; // Not enough data to smooth380  381  const smoothed = [];382  const halfWindow = Math.floor(windowSize / 2);383  384  for (let i = 0; i < data.length; i++) {385    const start = Math.max(0, i - halfWindow);386    const end = Math.min(data.length - 1, i + halfWindow);387    388    let sum = 0;389    let count = 0;390    391    // Calculate weighted average with more weight to center point392    for (let j = start; j <= end; j++) {393      const distance = Math.abs(j - i);394      const weight = distance === 0 ? 2 : (distance === 1 ? 1.5 : 1); // Center gets more weight395      sum += data[j].value * weight;396      count += weight;397    }398    399    smoothed.push({400      step: data[i].step,401      value: sum / count402    });403  }404  405  return smoothed;406}407 408/**409 * Applies smoothing to all runs in metric data410 * @param {Object} metricData - Object with run names as keys and data arrays as values411 * @param {number} windowSize - Size of the smoothing window412 * @returns {Object} Smoothed metric data413 */414export function smoothMetricData(metricData, windowSize = 5) {415  if (!metricData) return metricData;416  417  const smoothedData = {};418  Object.keys(metricData).forEach(runName => {419    smoothedData[runName] = smoothData(metricData[runName], windowSize);420  });421  422  return smoothedData;423}424