summaryrefslogtreecommitdiff
path: root/web-timeplot/src/test-data-generators.js
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-11-25 21:22:17 -0500
committergrothedev <grothedev@gmail.com>2025-11-25 21:22:17 -0500
commit43420f2987b76aa7ede0012e1998ba8d61419bc9 (patch)
treebbb7214c9105fbfeca4ba82056cdfc6fb107b5c2 /web-timeplot/src/test-data-generators.js
parent2c1b37a5b0c4962b405a85768b9b8cdd5c4f1097 (diff)
pushing claude changes before i take another route
Diffstat (limited to 'web-timeplot/src/test-data-generators.js')
-rw-r--r--web-timeplot/src/test-data-generators.js530
1 files changed, 530 insertions, 0 deletions
diff --git a/web-timeplot/src/test-data-generators.js b/web-timeplot/src/test-data-generators.js
new file mode 100644
index 0000000..02bc0ad
--- /dev/null
+++ b/web-timeplot/src/test-data-generators.js
@@ -0,0 +1,530 @@
+/**
+ * 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<number>} 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
+ * }));
+ */