|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class AdaptiveSampler { |
|
|
constructor(options = {}) { |
|
|
this.options = { |
|
|
maxPoints: 400, |
|
|
targetPoints: 200, |
|
|
preserveFeatures: true, |
|
|
adaptiveStrategy: 'smart', |
|
|
smoothingWindow: 3, |
|
|
...options |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
needsSampling(dataLength) { |
|
|
return dataLength > this.options.maxPoints; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uniformSampling(data) { |
|
|
const step = Math.ceil(data.length / this.options.targetPoints); |
|
|
const sampledData = []; |
|
|
const sampledIndices = []; |
|
|
|
|
|
|
|
|
sampledData.push(data[0]); |
|
|
sampledIndices.push(0); |
|
|
|
|
|
for (let i = step; i < data.length - 1; i += step) { |
|
|
sampledData.push(data[i]); |
|
|
sampledIndices.push(i); |
|
|
} |
|
|
|
|
|
|
|
|
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' |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
smartSampling(data) { |
|
|
const targetPoints = this.options.targetPoints; |
|
|
const features = this.detectFeatures(data); |
|
|
|
|
|
|
|
|
const criticalPoints = new Set([0, data.length - 1]); |
|
|
|
|
|
|
|
|
features.peaks.forEach(idx => criticalPoints.add(idx)); |
|
|
features.valleys.forEach(idx => criticalPoints.add(idx)); |
|
|
features.inflectionPoints.forEach(idx => criticalPoints.add(idx)); |
|
|
|
|
|
|
|
|
const remaining = targetPoints - criticalPoints.size; |
|
|
if (remaining > 0) { |
|
|
const logSamples = this.generateLogSpacing(data.length, remaining); |
|
|
logSamples.forEach(idx => criticalPoints.add(idx)); |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const visibleTargetPoints = Math.floor(this.options.targetPoints * 0.7); |
|
|
const contextTargetPoints = this.options.targetPoints - visibleTargetPoints; |
|
|
|
|
|
|
|
|
const visibleSample = this.smartSampling(viewData); |
|
|
|
|
|
|
|
|
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: [] }; |
|
|
|
|
|
|
|
|
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' |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
if (current > prev && current > next) { |
|
|
|
|
|
const localMax = Math.max( |
|
|
...data.slice(i - window, i + window + 1).map(d => d.value) |
|
|
); |
|
|
if (current === localMax) { |
|
|
peaks.push(i); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateLogSpacing(totalLength, count) { |
|
|
const indices = []; |
|
|
for (let i = 1; i <= count; i++) { |
|
|
const progress = i / (count + 1); |
|
|
|
|
|
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)]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sampleByVariation(data, targetPoints) { |
|
|
const variations = []; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const avgNeighbors = (prev + next) / 2; |
|
|
const variation = Math.abs(curr - avgNeighbors); |
|
|
|
|
|
variations.push({ index: i, variation }); |
|
|
} |
|
|
|
|
|
|
|
|
variations.sort((a, b) => b.variation - a.variation); |
|
|
|
|
|
return variations.slice(0, targetPoints).map(v => v.index); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getFullDataForRange(originalData, samplingInfo, startStep, endStep) { |
|
|
|
|
|
|
|
|
const startIdx = originalData.findIndex(d => d.step >= startStep); |
|
|
const endIdx = originalData.findIndex(d => d.step > endStep); |
|
|
|
|
|
return originalData.slice(startIdx, endIdx === -1 ? undefined : endIdx); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const trackioSampler = new AdaptiveSampler({ |
|
|
maxPoints: 400, |
|
|
targetPoints: 200, |
|
|
preserveFeatures: true, |
|
|
adaptiveStrategy: 'smart' |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function sampleLargeDataset(metricData, options = {}) { |
|
|
const sampler = new AdaptiveSampler(options); |
|
|
return sampler.sampleMetricData(metricData); |
|
|
} |
|
|
|