thibaud frere
cleanup
1ee6ce7
// Adaptive Sampling System for Large Datasets
// Inspired by Weights & Biases approach to handle massive time series
/**
* Adaptive Sampler - Intelligently reduces data points while preserving visual fidelity
*/
export class AdaptiveSampler {
constructor(options = {}) {
this.options = {
maxPoints: 400, // Threshold to trigger sampling
targetPoints: 200, // Target number of points after sampling
preserveFeatures: true, // Preserve important peaks/valleys
adaptiveStrategy: 'smart', // 'uniform', 'smart', 'lod'
smoothingWindow: 3, // Window for feature detection
...options
};
}
/**
* Determine if sampling is necessary
*/
needsSampling(dataLength) {
return dataLength > this.options.maxPoints;
}
/**
* Main entry point for sampling
*/
sampleSeries(data, strategy = null) {
if (!Array.isArray(data) || data.length === 0) {
return { data: [], sampledIndices: [], compressionRatio: 1 };
}
const actualStrategy = strategy || this.options.adaptiveStrategy;
if (!this.needsSampling(data.length)) {
return {
data: data.slice(),
sampledIndices: data.map((_, i) => i),
compressionRatio: 1,
strategy: 'none'
};
}
console.log(`🎯 Sampling ${data.length} points with strategy: ${actualStrategy}`);
switch (actualStrategy) {
case 'uniform':
return this.uniformSampling(data);
case 'smart':
return this.smartSampling(data);
case 'lod':
return this.lodSampling(data);
default:
return this.smartSampling(data);
}
}
/**
* Sampling uniforme - simple mais pas optimal
*/
uniformSampling(data) {
const step = Math.ceil(data.length / this.options.targetPoints);
const sampledData = [];
const sampledIndices = [];
// Toujours inclure le premier et dernier point
sampledData.push(data[0]);
sampledIndices.push(0);
for (let i = step; i < data.length - 1; i += step) {
sampledData.push(data[i]);
sampledIndices.push(i);
}
// Toujours inclure le dernier point
if (data.length > 1) {
sampledData.push(data[data.length - 1]);
sampledIndices.push(data.length - 1);
}
return {
data: sampledData,
sampledIndices,
compressionRatio: sampledData.length / data.length,
strategy: 'uniform'
};
}
/**
* Smart sampling - préserve les features importantes
* Inspiré de l'algorithme de Douglas-Peucker adapté pour les time series
*/
smartSampling(data) {
const targetPoints = this.options.targetPoints;
const features = this.detectFeatures(data);
// Étape 1: Points critiques (début, fin, features importantes)
const criticalPoints = new Set([0, data.length - 1]);
// Ajouter les features détectés
features.peaks.forEach(idx => criticalPoints.add(idx));
features.valleys.forEach(idx => criticalPoints.add(idx));
features.inflectionPoints.forEach(idx => criticalPoints.add(idx));
// Étape 2: Répartition logarithmique pour préserver la densité
const remaining = targetPoints - criticalPoints.size;
if (remaining > 0) {
const logSamples = this.generateLogSpacing(data.length, remaining);
logSamples.forEach(idx => criticalPoints.add(idx));
}
// Étape 3: Densité adaptive dans les zones de changement
if (criticalPoints.size < targetPoints) {
const variationSamples = this.sampleByVariation(data, targetPoints - criticalPoints.size);
variationSamples.forEach(idx => criticalPoints.add(idx));
}
const sampledIndices = Array.from(criticalPoints).sort((a, b) => a - b);
const sampledData = sampledIndices.map(idx => data[idx]);
return {
data: sampledData,
sampledIndices,
compressionRatio: sampledData.length / data.length,
strategy: 'smart',
features
};
}
/**
* Level-of-Detail sampling - adaptatif selon le zoom/contexte
*/
lodSampling(data, viewportStart = 0, viewportEnd = 1, zoomLevel = 1) {
const viewStart = Math.floor(viewportStart * data.length);
const viewEnd = Math.ceil(viewportEnd * data.length);
const viewData = data.slice(viewStart, viewEnd);
// Plus de détails dans la zone visible
const visibleTargetPoints = Math.floor(this.options.targetPoints * 0.7);
const contextTargetPoints = this.options.targetPoints - visibleTargetPoints;
// Sampling dense dans la zone visible
const visibleSample = this.smartSampling(viewData);
// Sampling sparse dans le contexte
const beforeContext = data.slice(0, viewStart);
const afterContext = data.slice(viewEnd);
const beforeSample = beforeContext.length > 0 ?
this.uniformSampling(beforeContext) : { data: [], sampledIndices: [] };
const afterSample = afterContext.length > 0 ?
this.uniformSampling(afterContext) : { data: [], sampledIndices: [] };
// Combiner les résultats
const combinedData = [
...beforeSample.data,
...visibleSample.data,
...afterSample.data
];
const combinedIndices = [
...beforeSample.sampledIndices,
...visibleSample.sampledIndices.map(idx => idx + viewStart),
...afterSample.sampledIndices.map(idx => idx + viewEnd)
];
return {
data: combinedData,
sampledIndices: combinedIndices,
compressionRatio: combinedData.length / data.length,
strategy: 'lod'
};
}
/**
* Détection des features importantes dans la série
*/
detectFeatures(data) {
const peaks = [];
const valleys = [];
const inflectionPoints = [];
const window = this.options.smoothingWindow;
for (let i = window; i < data.length - window; i++) {
const current = data[i].value;
const prev = data[i - 1].value;
const next = data[i + 1].value;
// Détection des pics locaux
if (current > prev && current > next) {
// Vérifier si c'est un pic significatif
const localMax = Math.max(
...data.slice(i - window, i + window + 1).map(d => d.value)
);
if (current === localMax) {
peaks.push(i);
}
}
// Détection des vallées locales
if (current < prev && current < next) {
const localMin = Math.min(
...data.slice(i - window, i + window + 1).map(d => d.value)
);
if (current === localMin) {
valleys.push(i);
}
}
// Détection des points d'inflection (changement de courbure)
if (i >= 2 && i < data.length - 2) {
const trend1 = data[i].value - data[i - 2].value;
const trend2 = data[i + 2].value - data[i].value;
if (Math.sign(trend1) !== Math.sign(trend2) && Math.abs(trend1) > 0.01 && Math.abs(trend2) > 0.01) {
inflectionPoints.push(i);
}
}
}
return { peaks, valleys, inflectionPoints };
}
/**
* Génère des indices avec espacement logarithmique
*/
generateLogSpacing(totalLength, count) {
const indices = [];
for (let i = 1; i <= count; i++) {
const progress = i / (count + 1);
// Fonction logarithmique pour plus de densité au début
const logProgress = Math.log(1 + progress * (Math.E - 1)) / Math.log(Math.E);
const index = Math.floor(logProgress * (totalLength - 1));
indices.push(Math.max(1, Math.min(totalLength - 2, index)));
}
return [...new Set(indices)]; // Remove duplicates
}
/**
* Sampling based on local variation
*/
sampleByVariation(data, targetPoints) {
const variations = [];
// Calculer la variation locale pour chaque point
for (let i = 1; i < data.length - 1; i++) {
const prev = data[i - 1].value;
const curr = data[i].value;
const next = data[i + 1].value;
// Variation = différence avec la moyenne des voisins
const avgNeighbors = (prev + next) / 2;
const variation = Math.abs(curr - avgNeighbors);
variations.push({ index: i, variation });
}
// Trier par variation décroissante et prendre les plus importantes
variations.sort((a, b) => b.variation - a.variation);
return variations.slice(0, targetPoints).map(v => v.index);
}
/**
* Applique le sampling sur un objet de données complètes (multi-run)
*/
sampleMetricData(metricData, strategy = null) {
const sampledData = {};
const samplingInfo = {};
Object.keys(metricData).forEach(runName => {
const runData = metricData[runName] || [];
const result = this.sampleSeries(runData, strategy);
sampledData[runName] = result.data;
samplingInfo[runName] = {
originalLength: runData.length,
sampledLength: result.data.length,
compressionRatio: result.compressionRatio,
strategy: result.strategy,
sampledIndices: result.sampledIndices
};
});
return { sampledData, samplingInfo };
}
/**
* Reconstruit les données complètes pour une zone spécifique (pour le zoom)
*/
getFullDataForRange(originalData, samplingInfo, startStep, endStep) {
// This method would allow recovering more details
// quand l'utilisateur zoom sur une zone spécifique
const startIdx = originalData.findIndex(d => d.step >= startStep);
const endIdx = originalData.findIndex(d => d.step > endStep);
return originalData.slice(startIdx, endIdx === -1 ? undefined : endIdx);
}
}
/**
* Instance globale configurée pour TrackIO
*/
export const trackioSampler = new AdaptiveSampler({
maxPoints: 400,
targetPoints: 200,
preserveFeatures: true,
adaptiveStrategy: 'smart'
});
/**
* Fonction utilitaire pour usage direct
*/
export function sampleLargeDataset(metricData, options = {}) {
const sampler = new AdaptiveSampler(options);
return sampler.sampleMetricData(metricData);
}