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