diff options
| author | Thomas Grothe <grothe.tr@gmail.com> | 2026-04-30 00:53:13 -0400 |
|---|---|---|
| committer | Thomas Grothe <grothe.tr@gmail.com> | 2026-04-30 00:53:13 -0400 |
| commit | 73d75835e18a33c7f6c1b09bbcef93b16a7a9bfa (patch) | |
| tree | e079c6c45416333e29cf900831c07619a87d5c39 /web-timeplot/ARCHITECTURE.md | |
| parent | a1c95e72bea26f554eb05916d6fc584927367159 (diff) | |
redo timeplot web
Diffstat (limited to 'web-timeplot/ARCHITECTURE.md')
| -rw-r--r-- | web-timeplot/ARCHITECTURE.md | 541 |
1 files changed, 139 insertions, 402 deletions
diff --git a/web-timeplot/ARCHITECTURE.md b/web-timeplot/ARCHITECTURE.md index b49c0b0..1cfc3f1 100644 --- a/web-timeplot/ARCHITECTURE.md +++ b/web-timeplot/ARCHITECTURE.md @@ -2,433 +2,170 @@ ## Overview -This document describes the clean, modular architecture for TimePlot's data visualization system. - -## Design Principles - -**Separation of Concerns**: Data generation, data provision, and visualization are completely separate. - -- **Plots don't generate data** - They only display it -- **Data sources don't know about visualization** - They only produce data -- **Connections manage the flow** - They link sources to plots - -This architecture allows you to: -- Easily swap data sources without changing visualization -- Reuse plots with different data -- Test components independently -- Support multiple data types (real-time, synthetic, replay, etc.) - -## Architecture Layers - -``` -┌─────────────────────────────────────────────────┐ -│ Application Layer (main.js) │ -│ - Manages app lifecycle │ -│ - Creates plots and sources │ -│ - Sets up connections │ -└─────────────────────────────────────────────────┘ - │ - ├──────────────┬──────────────┐ - ↓ ↓ ↓ - ┌────────────────┐ ┌─────────────┐ ┌─────────────┐ - │ Connections │ │ Plots │ │ Sources │ - │ (Glue Layer) │ │ (Display) │ │ (Data) │ - └────────────────┘ └─────────────┘ └─────────────┘ -``` - -## Core Components - -### 1. Data Generators (`test-data-generators.js`) - -**Purpose**: Generate mathematical patterns for testing - -**Classes**: -- `DataGenerator` - Base class with common functionality -- `SineWaveGenerator` - Classic sine waves -- `SquareWaveGenerator` - Digital square waves -- `PerlinNoiseGenerator` - Smooth noise -- `ChirpGenerator` - Frequency sweeps -- `CompositeGenerator` - Combine multiple generators -- Many more... - -**Usage**: -```javascript -const generator = new SineWaveGenerator({ - frequency: 2.0, - amplitude: 30, - sampleRate: 100, -}); - -// Generate a line of points -const points = generator.generateLine(100, 800); -``` - -### 2. Data Sources (`data-sources.js`) - -**Purpose**: Provide data to plots via events - -**Key Concept**: Sources emit events when data is ready. They know *when* and *how* to provide data, but not *where* it goes. - -**Base Class**: -```javascript -class DataSource extends EventEmitter { - start() // Begin providing data - stop() // Stop providing data - emitLine() // Emit a complete line of data - emitPoint() // Emit a single data point +The restarted TimePlot app is built around five small systems: + +1. **Store** — single source of truth for app state +2. **Time controller** — advances real time and plot time +3. **Source registry** — owns active data source lifecycle +4. **Plot view** — renders visible samples and handles hover picking +5. **Panel manager** — builds the DOM shell and user controls + +The current implementation is intentionally compact, but each system is already separated enough to grow without turning the app into a monolith again. + +## Runtime flow + +```text +TimeController.tick() + ↓ +Store.time updated + ↓ +SourceRegistry.update(plotTime) + ↓ +SyntheticWaveSource emits samples + ↓ +PlotBuffer stores bounded history + ↓ +TimeplotView renders visible window + ↓ +PanelManager reflects status + tooltip +``` + +## Core principles + +### 1. Time is explicit + +Plot time is not inferred from frame count or rendering. It is advanced by `TimeController`, which makes features like pause, speed changes, replay, and stepping straightforward. + +### 2. Data sources are replaceable + +`SourceRegistry` talks to the active source through a tiny shared contract: + +- `start(startTimeMs)` +- `update(currentPlotTimeMs)` +- `reset(startTimeMs)` +- `updateConfig(partialConfig)` + +That keeps future WebSocket, file replay, database, or simulated sources easy to add. + +### 3. Rendering stays focused + +`TimeplotView` does not own application state or source orchestration. It receives state plus visible points and turns that into pixels. Hover detection also lives close to rendering because it depends on screen-space positions. + +### 4. UI panels stay in the DOM + +The plot is GPU-rendered with PixiJS. Controls, labels, and config panels stay in regular DOM so they are easy to iterate on, inspect, and restyle. + +### 5. Composition happens at the edge + +`create-app.js` is the composition root. It wires together store, time, sources, plot, UI, keyboard shortcuts, and the frame loop. That keeps the rest of the modules simple and testable. + +## Current state shape + +```js +{ + app: { title, renderer }, + time: { + realNowMs, + realElapsedMs, + plotTimeMs, + speed, + paused, + }, + plot: { + showGrid, + showPoints, + windowDurationMs, + maxPoints, + valueRange, + hoveredPoint, + tooltip, + }, + source: { + activeId, + preset, + sampleRateHz, + amplitude, + noise, + }, + panels: { + status, + source, + config, + help, + }, } ``` -**Events**: -- `'line'` - Complete line of data ready: `{points, timestamp, metadata}` -- `'point'` - Single data point ready: `{value, timestamp}` -- `'error'` - Error occurred: `{error}` - -**Available Sources**: - -- **SyntheticDataSource** - Uses test generators, emits lines periodically -- **FunctionDataSource** - Evaluates a function (x,t) => y -- **StreamingDataSource** - Emits individual points at a sample rate -- **WebSocketDataSource** - Receives real-time data from WebSocket -- **CSVDataSource** - Replays data from CSV files -- **CompositeDataSource** - Combines multiple sources - -**Example**: -```javascript -// Create source -const source = new SyntheticDataSource({ - generator: new SineWaveGenerator({ frequency: 2.0 }), - pointsPerLine: 100, - width: 800, - lineInterval: 100, // ms between lines -}); - -// Listen to events -source.on('line', (data) => { - console.log('Received line:', data.points); -}); - -// Start generating -source.start(); -``` +## Modules -### 3. Plots (`timeseries-plot.js`) +### `src/core/store.js` -**Purpose**: Pure visualization - display data, nothing else +A tiny centralized store. It currently favors clarity over abstraction-heavy patterns. -**Key Concept**: Plots receive data via method calls. They know *how* to display data, but not *where* it comes from. +### `src/core/time-controller.js` -**API**: -```javascript -class TimeSeriesPlot { - // Data input - addLine(points, metadata) // Add a line of data - addDataPoint(value, timestamp) // Add a single point - clearData() // Clear all data +Owns playback semantics: - // Display control - setGridVisible(visible) - setScrollSpeed(speed) - setVerticalScale(scale) - setTitle(title) +- pause/resume +- speed control +- plot time reset +- frame-to-frame delta handling - // Frame update - update() // Call every frame to scroll/render -} -``` +### `src/data/synthetic-wave-source.js` -**Example**: -```javascript -// Create plot -const plot = new TimeSeriesPlot({ - x: 0, - y: 0, - width: 800, - height: 600, - title: 'My Plot', - showGrid: true, -}); - -// Add to PixiJS stage -app.stage.addChild(plot.container); - -// Receive data -plot.addLine([ - {x: 0, y: 10}, - {x: 100, y: 20}, - {x: 200, y: 15}, -]); - -// Update every frame -app.ticker.add(() => plot.update()); -``` +Generates sample streams from a preset waveform. Right now it supports: -### 4. Connections (`plot-connections.js`) +- `telemetry` +- `chirp` +- `burst` -**Purpose**: Link data sources to plots +### `src/plot/plot-buffer.js` -**Key Concept**: Connections subscribe to source events and forward data to plots. They handle timing, buffering, and data transformation. +Maintains bounded history so rendering and hover picking only operate on a manageable number of samples. -**Connection Types**: - -**DirectConnection** - Lines go straight from source to plot -```javascript -const connection = new DirectConnection(source, plot); -connection.connect(); -``` +### `src/plot/timeplot-view.js` -**BufferedConnection** - Buffers individual points into lines -```javascript -const connection = new BufferedConnection(source, plot, { - bufferSize: 100, // Points per line - bufferTimeout: 1000, // Max time to wait (ms) -}); -connection.connect(); -``` +Owns Pixi initialization, plotting, grid drawing, and nearest-point hover selection. -**SynchronizedConnection** - Synchronizes multiple sources -```javascript -const connection = new SynchronizedConnection([source1, source2], plot, { - syncMode: 'wait-for-all', -}); -connection.connect(); -``` +### `src/ui/panel-manager.js` -**Helper Functions**: -```javascript -// Quick setup for synthetic data -const conn = connectSyntheticData(generator, plot, { - lineInterval: 100, -}); +Creates: -// Quick setup for function-based -const conn = connectFunction((x, t) => Math.sin(x * 10 + t), plot); +- top transport bar +- panel toggle buttons +- status panel +- data source panel +- config panel +- help panel +- floating tooltip -// All-in-one setup -const {plot, source, connection} = createConnectedPlot(app, plotConfig, sourceConfig); -``` +## Why this is a better baseline -## Data Flow +The old project had useful ideas but too many concerns were mixed together. The new baseline is better because: -### Scenario 1: Synthetic Data (Test Pattern) +- transport logic is separate from rendering +- data generation is separate from app wiring +- UI is separate from GPU drawing +- state is centralized and observable +- adding a new source or panel no longer requires rewriting the whole app -``` -DataGenerator SyntheticDataSource DirectConnection TimeSeriesPlot - │ │ │ │ - │ │ start() │ │ - │ ├───────────────────────>│ │ - │ │ │ │ - │ generateLine() │ │ │ - │<────────────────────────┤ │ │ - │ │ │ │ - │ returns points[] │ │ │ - ├────────────────────────>│ │ │ - │ │ │ │ - │ │ emit('line', data) │ │ - │ ├───────────────────────>│ │ - │ │ │ │ - │ │ │ addLine(points) │ - │ │ ├─────────────────────>│ - │ │ │ │ - │ │ │ │ update() - │ │ │ │ (scroll & render) -``` +## Recommended next steps -### Scenario 2: Real-Time Streaming +### Near term -``` -External System WebSocketDataSource BufferedConnection TimeSeriesPlot - │ │ │ │ - │ WebSocket message │ │ │ - ├────────────────────────>│ │ │ - │ │ │ │ - │ │ emit('point', value) │ │ - │ ├───────────────────────>│ │ - │ │ │ │ - │ │ │ buffer.push(value) │ - │ │ │ │ - │ │ emit('point') │ │ - │ ├───────────────────────>│ │ - │ │ │ │ - │ │ (continue...) │ │ - │ │ │ │ - │ │ │ buffer full! │ - │ │ │ │ - │ │ │ flush() │ - │ │ │ addLine(buffered) │ - │ │ ├─────────────────────>│ -``` - -## Usage Patterns - -### Pattern 1: Simple Test Visualization - -```javascript -import { TimeSeriesPlot } from './timeseries-plot.js'; -import { connectSyntheticData } from './plot-connections.js'; -import { TestDataFactory } from './test-data-generators.js'; - -// Create plot -const plot = new TimeSeriesPlot({...}); -app.stage.addChild(plot.container); - -// Connect data -const connection = connectSyntheticData( - TestDataFactory.createSimpleSine(30), - plot, - { lineInterval: 100 } -); - -// Update loop -app.ticker.add(() => plot.update()); -``` - -### Pattern 2: Swap Data Sources - -```javascript -// Start with one source -let connection = connectSyntheticData(generator1, plot); - -// Later, switch to different data -connection.disconnect(); -connection = connectSyntheticData(generator2, plot); -``` - -### Pattern 3: Multiple Plots, Different Data - -```javascript -const plot1 = new TimeSeriesPlot({x: 0, y: 0, ...}); -const plot2 = new TimeSeriesPlot({x: 800, y: 0, ...}); - -connectSyntheticData(sineGenerator, plot1); -connectSyntheticData(noiseGenerator, plot2); - -app.ticker.add(() => { - plot1.update(); - plot2.update(); -}); -``` - -### Pattern 4: Real-Time WebSocket Data - -```javascript -const plot = new TimeSeriesPlot({...}); - -const source = new WebSocketDataSource({ - url: 'ws://localhost:8080/data' -}); - -const connection = new BufferedConnection(source, plot, { - bufferSize: 100, -}); -connection.connect(); - -app.ticker.add(() => plot.update()); -``` - -### Pattern 5: Custom Function - -```javascript -const plot = new TimeSeriesPlot({...}); - -const connection = connectFunction( - (x, t) => Math.sin(x * 10 + t * 2) + Math.cos(x * 5 - t), - plot, - { lineInterval: 100, amplitude: 30 } -); - -app.ticker.add(() => plot.update()); -``` - -## File Organization - -``` -web-timeplot/src/ -├── test-data-generators.js # Math generators for test patterns -├── data-sources.js # Data provision (events) -├── timeseries-plot.js # Pure visualization -├── plot-connections.js # Glue layer -├── example-usage.js # Complete examples -├── main.js # Application entry point -├── state.js # App state management -└── waterfall.js # (Legacy - can be replaced) -``` - -## Migration Path - -### From Old WaterfallGraph to New Architecture - -**Old way** (tightly coupled): -```javascript -const graph = new WaterfallGraph({...}); -// Graph generates its own data -``` - -**New way** (separated): -```javascript -const plot = new TimeSeriesPlot({...}); -const source = new SyntheticDataSource({...}); -const connection = new DirectConnection(source, plot); -connection.connect(); -``` - -## Extension Points - -### Adding New Data Sources - -Extend `DataSource` and implement: -- `start()` - Begin providing data -- `stop()` - Stop providing data -- Call `emitLine()` or `emitPoint()` when data is ready - -Example: -```javascript -class MyCustomSource extends DataSource { - start() { - super.start(); - // Start your data provision mechanism - this.interval = setInterval(() => { - const points = [...]; // Generate points - this.emitLine(points); - }, 100); - } - - stop() { - super.stop(); - clearInterval(this.interval); - } -} -``` - -### Adding New Connection Types - -Extend `PlotConnection` and implement `setupSubscriptions()`: - -```javascript -class MyCustomConnection extends PlotConnection { - setupSubscriptions() { - const unsub = this.source.on('line', (data) => { - // Transform data as needed - const transformed = this.transformData(data); - this.plot.addLine(transformed.points); - }); - this.subscriptions.push(unsub); - } -} -``` +- add persisted settings for panel visibility and playback preferences +- support multiple plot panes from a shared timebase +- add line/series definitions instead of a single hard-coded signal -## Benefits of This Architecture +### Medium term -1. **Testability** - Each component can be tested independently -2. **Reusability** - Plots work with any data source -3. **Flexibility** - Easy to add new data sources or visualizations -4. **Maintainability** - Clear responsibilities, easy to understand -5. **Performance** - Can optimize each layer independently -6. **Real-world ready** - Supports actual data sources (WebSocket, files, etc.) +- add schema-aware input adapters +- add WebSocket and replay-file sources +- add panel docking/layout persistence +- add markers, cursors, and annotations -## Next Steps +### Longer term -- Replace old `waterfall.js` usage with new `TimeSeriesPlot` -- Create real data sources for your application -- Add more visualization types (heatmap, spectrogram, etc.) -- Add data processing layer (filtering, FFT, etc.) +- multi-stream synchronization +- richer interaction model for HID input mapping +- plug-in style source and panel registration |
