/** * Test Data Generators - Classes for generating fake/test data patterns * * These generators produce various types of synthetic data for testing * and visualizing the waterfall graphs with realistic patterns. */ /** * Base class for all data generators */ class DataGenerator { constructor(config = {}) { this.sampleRate = config.sampleRate || 100; // Samples per second this.amplitude = config.amplitude || 1.0; this.offset = config.offset || 0.0; this.time = 0; } /** * Generate a single sample at the current time * @returns {number} The generated value */ sample() { throw new Error('sample() must be implemented by subclass'); } /** * Generate an array of samples * @param {number} count - Number of samples to generate * @returns {Array} Array of generated values */ generateSamples(count) { const samples = []; for (let i = 0; i < count; i++) { samples.push(this.sample()); this.time += 1 / this.sampleRate; } return samples; } /** * Generate a line of points for waterfall display * @param {number} pointCount - Number of points in the line * @param {number} width - Width of the display area * @returns {Array<{x: number, y: number}>} Array of points */ generateLine(pointCount, width) { const points = []; const samples = this.generateSamples(pointCount); for (let i = 0; i < pointCount; i++) { const x = (i / pointCount) * width; const y = samples[i] * this.amplitude + this.offset; points.push({ x, y }); } return points; } reset() { this.time = 0; } } /** * Sine Wave Generator - Classic sinusoidal wave */ export class SineWaveGenerator extends DataGenerator { constructor(config = {}) { super(config); this.frequency = config.frequency || 1.0; // Hz this.phase = config.phase || 0.0; // Radians } sample() { const value = Math.sin(2 * Math.PI * this.frequency * this.time + this.phase); return value; } } /** * Square Wave Generator - Digital-style square wave */ export class SquareWaveGenerator extends DataGenerator { constructor(config = {}) { super(config); this.frequency = config.frequency || 1.0; this.dutyCycle = config.dutyCycle || 0.5; // 0.0 to 1.0 } sample() { const period = 1 / this.frequency; const phase = (this.time % period) / period; return phase < this.dutyCycle ? 1.0 : -1.0; } } /** * Sawtooth Wave Generator - Linear ramp wave */ export class SawtoothWaveGenerator extends DataGenerator { constructor(config = {}) { super(config); this.frequency = config.frequency || 1.0; } sample() { const period = 1 / this.frequency; const phase = (this.time % period) / period; return 2 * phase - 1; // -1 to 1 } } /** * Triangle Wave Generator - Linear up/down wave */ export class TriangleWaveGenerator extends DataGenerator { constructor(config = {}) { super(config); this.frequency = config.frequency || 1.0; } sample() { const period = 1 / this.frequency; const phase = (this.time % period) / period; return phase < 0.5 ? 4 * phase - 1 : 3 - 4 * phase; } } /** * White Noise Generator - Random noise */ export class WhiteNoiseGenerator extends DataGenerator { sample() { return Math.random() * 2 - 1; // -1 to 1 } } /** * Pink Noise Generator - 1/f noise (more realistic than white noise) */ export class PinkNoiseGenerator extends DataGenerator { constructor(config = {}) { super(config); // Paul Kellet's refined method this.b0 = 0; this.b1 = 0; this.b2 = 0; this.b3 = 0; this.b4 = 0; this.b5 = 0; this.b6 = 0; } sample() { const white = Math.random() * 2 - 1; this.b0 = 0.99886 * this.b0 + white * 0.0555179; this.b1 = 0.99332 * this.b1 + white * 0.0750759; this.b2 = 0.96900 * this.b2 + white * 0.1538520; this.b3 = 0.86650 * this.b3 + white * 0.3104856; this.b4 = 0.55000 * this.b4 + white * 0.5329522; this.b5 = -0.7616 * this.b5 - white * 0.0168980; const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; this.b6 = white * 0.115926; return pink * 0.11; // Normalize } } /** * Perlin Noise Generator - Smooth, continuous noise */ export class PerlinNoiseGenerator extends DataGenerator { constructor(config = {}) { super(config); this.frequency = config.frequency || 1.0; this.octaves = config.octaves || 4; this.persistence = config.persistence || 0.5; } // Simple 1D Perlin-like noise noise(x) { const i = Math.floor(x); const f = x - i; // Fade curve const u = f * f * (3 - 2 * f); // Hash function for pseudo-random gradients const hash = (n) => { n = (n << 13) ^ n; return (1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0); }; return (1 - u) * hash(i) + u * hash(i + 1); } sample() { let value = 0; let amplitude = 1; let frequency = this.frequency; let maxValue = 0; for (let i = 0; i < this.octaves; i++) { value += this.noise(this.time * frequency) * amplitude; maxValue += amplitude; amplitude *= this.persistence; frequency *= 2; } return value / maxValue; } } /** * Pulse/Spike Generator - Random spikes/pulses */ export class PulseGenerator extends DataGenerator { constructor(config = {}) { super(config); this.pulseRate = config.pulseRate || 0.05; // Probability per sample this.pulseWidth = config.pulseWidth || 0.01; // Duration in seconds this.pulseAmplitude = config.pulseAmplitude || 1.0; this.currentPulse = null; } sample() { // Check if we're in a pulse if (this.currentPulse) { if (this.time >= this.currentPulse.endTime) { this.currentPulse = null; } else { return this.pulseAmplitude; } } // Random chance to start new pulse if (Math.random() < this.pulseRate) { this.currentPulse = { startTime: this.time, endTime: this.time + this.pulseWidth, }; return this.pulseAmplitude; } return 0; } } /** * Burst Generator - Bursts of activity with quiet periods */ export class BurstGenerator extends DataGenerator { constructor(config = {}) { super(config); this.burstDuration = config.burstDuration || 1.0; // Seconds this.quietDuration = config.quietDuration || 2.0; // Seconds this.burstFrequency = config.burstFrequency || 5.0; // Hz during burst this.currentState = 'quiet'; this.stateStartTime = 0; } sample() { const elapsed = this.time - this.stateStartTime; // State transitions if (this.currentState === 'quiet' && elapsed >= this.quietDuration) { this.currentState = 'burst'; this.stateStartTime = this.time; } else if (this.currentState === 'burst' && elapsed >= this.burstDuration) { this.currentState = 'quiet'; this.stateStartTime = this.time; } // Generate value based on state if (this.currentState === 'burst') { return Math.sin(2 * Math.PI * this.burstFrequency * this.time); } else { return 0; } } } /** * Chirp Generator - Frequency sweep signal */ export class ChirpGenerator extends DataGenerator { constructor(config = {}) { super(config); this.startFreq = config.startFreq || 0.5; // Hz this.endFreq = config.endFreq || 10.0; // Hz this.duration = config.duration || 5.0; // Seconds } sample() { const t = this.time % this.duration; const progress = t / this.duration; const freq = this.startFreq + (this.endFreq - this.startFreq) * progress; return Math.sin(2 * Math.PI * freq * t); } } /** * Composite Generator - Combine multiple generators */ export class CompositeGenerator extends DataGenerator { constructor(config = {}) { super(config); this.generators = config.generators || []; this.weights = config.weights || this.generators.map(() => 1.0); } sample() { let sum = 0; let weightSum = 0; for (let i = 0; i < this.generators.length; i++) { sum += this.generators[i].sample() * this.weights[i]; weightSum += this.weights[i]; } return weightSum > 0 ? sum / weightSum : 0; } generateSamples(count) { const samples = []; for (let i = 0; i < count; i++) { samples.push(this.sample()); // Advance all child generators this.generators.forEach(gen => gen.time += 1 / gen.sampleRate); } return samples; } } /** * FM (Frequency Modulation) Generator - One signal modulates another */ export class FMGenerator extends DataGenerator { constructor(config = {}) { super(config); this.carrierFreq = config.carrierFreq || 5.0; // Hz this.modulatorFreq = config.modulatorFreq || 0.5; // Hz this.modulationIndex = config.modulationIndex || 2.0; } sample() { const modulator = Math.sin(2 * Math.PI * this.modulatorFreq * this.time); const instantFreq = this.carrierFreq + this.modulationIndex * modulator; return Math.sin(2 * Math.PI * instantFreq * this.time); } } /** * Exponential Decay Generator - Exponentially decaying signal */ export class ExponentialDecayGenerator extends DataGenerator { constructor(config = {}) { super(config); this.decayRate = config.decayRate || 1.0; // 1/seconds this.oscillationFreq = config.oscillationFreq || 5.0; // Hz } sample() { const envelope = Math.exp(-this.decayRate * this.time); const oscillation = Math.sin(2 * Math.PI * this.oscillationFreq * this.time); return envelope * oscillation; } } /** * Step Function Generator - Random walk / brownian motion */ export class RandomWalkGenerator extends DataGenerator { constructor(config = {}) { super(config); this.stepSize = config.stepSize || 0.1; this.currentValue = 0; this.bounds = config.bounds || { min: -5, max: 5 }; } sample() { // Random step const step = (Math.random() - 0.5) * this.stepSize; this.currentValue += step; // Apply bounds this.currentValue = Math.max(this.bounds.min, Math.min(this.bounds.max, this.currentValue)); return this.currentValue; } } // ============================================================================ // Example Usage and Presets // ============================================================================ /** * Factory function to create common test scenarios */ export class TestDataFactory { static createSimpleSine(amplitude = 30) { return new SineWaveGenerator({ frequency: 2.0, amplitude: amplitude, sampleRate: 100, }); } static createNoisySine(amplitude = 30) { const sine = new SineWaveGenerator({ frequency: 2.0, amplitude: amplitude * 0.8, sampleRate: 100, }); const noise = new WhiteNoiseGenerator({ amplitude: amplitude * 0.2, sampleRate: 100, }); return new CompositeGenerator({ generators: [sine, noise], weights: [1.0, 1.0], }); } static createComplexPattern(amplitude = 30) { const low = new SineWaveGenerator({ frequency: 0.5, amplitude: amplitude * 0.4, sampleRate: 100, }); const mid = new SineWaveGenerator({ frequency: 3.0, amplitude: amplitude * 0.3, sampleRate: 100, }); const high = new SineWaveGenerator({ frequency: 8.0, amplitude: amplitude * 0.2, sampleRate: 100, }); const noise = new PinkNoiseGenerator({ amplitude: amplitude * 0.1, sampleRate: 100, }); return new CompositeGenerator({ generators: [low, mid, high, noise], weights: [1.0, 1.0, 1.0, 1.0], }); } static createBurstySignal(amplitude = 30) { return new BurstGenerator({ amplitude: amplitude, burstDuration: 0.5, quietDuration: 1.5, burstFrequency: 10.0, sampleRate: 100, }); } static createSmoothNoise(amplitude = 30) { return new PerlinNoiseGenerator({ amplitude: amplitude, frequency: 2.0, octaves: 3, persistence: 0.5, sampleRate: 100, }); } static createFrequencySweep(amplitude = 30) { return new ChirpGenerator({ amplitude: amplitude, startFreq: 0.5, endFreq: 10.0, duration: 3.0, sampleRate: 100, }); } static createModulatedSignal(amplitude = 30) { return new FMGenerator({ amplitude: amplitude, carrierFreq: 5.0, modulatorFreq: 0.3, modulationIndex: 3.0, sampleRate: 100, }); } static createRandomWalk(amplitude = 30) { return new RandomWalkGenerator({ stepSize: 0.5, bounds: { min: -amplitude, max: amplitude }, sampleRate: 100, }); } } /** * Example: How to use with WaterfallGraph * * // Create a generator * const generator = TestDataFactory.createComplexPattern(30); * * // In your graph's addLine method: * addLine(time, graphIdx) { * const line = { * points: generator.generateLine(this.pointsPerLine, this.width), * yOffset: 0, * color: this.generateColor(time), * }; * this.lines.push(line); * } * * // Or generate custom samples: * const samples = generator.generateSamples(100); * const points = samples.map((y, i) => ({ * x: (i / samples.length) * width, * y: y * })); */