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