diff options
Diffstat (limited to 'web-timeplot/src/timeseries-plot.js')
| -rw-r--r-- | web-timeplot/src/timeseries-plot.js | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/web-timeplot/src/timeseries-plot.js b/web-timeplot/src/timeseries-plot.js new file mode 100644 index 0000000..e35a704 --- /dev/null +++ b/web-timeplot/src/timeseries-plot.js @@ -0,0 +1,277 @@ +import { Container, Graphics, Text } from 'pixi.js'; + +/** + * TimeSeriesPlot - Pure visualization component for time-series data + * + * This class is responsible ONLY for displaying data, not generating it. + * It receives data points from external sources and renders them as a + * scrolling waterfall display. + * + * Architecture: + * - TimeSeriesPlot: Displays data (this file) + * - DataSource: Generates/provides data (data-sources.js) + * - Connection: Links sources to plots + */ +export class TimeSeriesPlot { + constructor(config) { + this.x = config.x || 0; + this.y = config.y || 0; + this.width = config.width || 800; + this.height = config.height || 600; + this.title = config.title || 'Time Series'; + this.baseColor = config.color || 0xff6666; + + // Container for all graphics + this.container = new Container(); + this.container.x = this.x; + this.container.y = this.y; + + // Graphics layers (order matters for rendering) + this.gridGraphics = new Graphics(); + this.linesGraphics = new Graphics(); + this.borderGraphics = new Graphics(); + + this.container.addChild(this.gridGraphics); + this.container.addChild(this.linesGraphics); + this.container.addChild(this.borderGraphics); + + // Title + this.titleText = new Text({ + text: this.title, + style: { + fontFamily: 'Arial', + fontSize: 18, + fill: 0xeeeeee, + } + }); + this.titleText.x = 10; + this.titleText.y = 10; + this.container.addChild(this.titleText); + + // Display state + this.lines = []; // Array of {points, yOffset, color, metadata} + this.maxLines = config.maxLines || 100; + this.showGrid = config.showGrid !== false; + + // Scrolling and scaling + this.scrollSpeed = config.scrollSpeed || 1.0; + this.verticalScale = config.verticalScale || 1.0; + + // Initial draw + this.draw(); + } + + // ======================================================================== + // Data Input API - This is how external sources send data to the plot + // ======================================================================== + + /** + * Add a new line of data to the plot + * @param {Array<{x: number, y: number}>} points - Array of points + * @param {Object} metadata - Optional metadata (color, timestamp, etc.) + */ + addLine(points, metadata = {}) { + const line = { + points: points, + yOffset: 0, + color: metadata.color || this.generateColor(Date.now() / 1000), + timestamp: metadata.timestamp || Date.now(), + metadata: metadata, + }; + + this.lines.push(line); + + // Limit number of lines + if (this.lines.length > this.maxLines) { + this.lines.shift(); + } + } + + /** + * Add a single data point (will be buffered into a line) + * This is useful for streaming real-time data + * @param {number} timestamp - Time of the data point + * @param {number} value - Value at this time + */ + addDataPoint(timestamp, value) { + // For now, this creates a single-point line + // In a more sophisticated version, this could buffer points + // until a full line is ready + const point = { x: this.width / 2, y: value }; + this.addLine([point], { timestamp }); + } + + /** + * Clear all data from the plot + */ + clearData() { + this.lines = []; + this.drawLines(); + } + + // ======================================================================== + // Update and Rendering + // ======================================================================== + + /** + * Update the plot - called each frame + * This handles scrolling and cleanup, but NOT data generation + */ + update() { + // Scroll existing lines down + this.scrollLines(); + + // Remove off-screen lines + this.lines = this.lines.filter(line => { + const scaledOffset = line.yOffset * this.verticalScale; + return scaledOffset < this.height + 50; + }); + + // Redraw + this.drawLines(); + } + + scrollLines() { + this.lines.forEach(line => { + line.yOffset += this.scrollSpeed; + }); + } + + draw() { + this.drawBorder(); + this.drawGrid(); + this.drawLines(); + } + + drawBorder() { + this.borderGraphics.clear(); + this.borderGraphics.rect(0, 0, this.width, this.height); + this.borderGraphics.stroke({ width: 2, color: 0x606070 }); + } + + drawGrid() { + this.gridGraphics.clear(); + + if (!this.showGrid) return; + + this.gridGraphics.alpha = 0.3; + + const divisions = 10; + const color = 0x4a7a9a; + + // Vertical lines + for (let i = 0; i <= divisions; i++) { + const x = (i / divisions) * this.width; + this.gridGraphics.moveTo(x, 0); + this.gridGraphics.lineTo(x, this.height); + this.gridGraphics.stroke({ width: 1, color }); + } + + // Horizontal lines + for (let i = 0; i <= divisions; i++) { + const y = (i / divisions) * this.height; + this.gridGraphics.moveTo(0, y); + this.gridGraphics.lineTo(this.width, y); + this.gridGraphics.stroke({ width: 1, color }); + } + } + + drawLines() { + this.linesGraphics.clear(); + + for (const line of this.lines) { + if (line.points.length < 2) continue; + + // Apply vertical scale to y positions + const scaledYOffset = line.yOffset * this.verticalScale; + + // Start path + const firstPoint = line.points[0]; + this.linesGraphics.moveTo(firstPoint.x, firstPoint.y + scaledYOffset); + + // Draw line strip + for (let i = 1; i < line.points.length; i++) { + const point = line.points[i]; + this.linesGraphics.lineTo(point.x, point.y + scaledYOffset); + } + + this.linesGraphics.stroke({ width: 2, color: line.color }); + } + } + + generateColor(time) { + // Cycle through colors based on time + const hue = (time * 0.1) % 1.0; + const r = Math.floor(Math.abs(Math.sin(hue * Math.PI * 2)) * 255); + const g = Math.floor(Math.abs(Math.sin((hue + 0.33) * Math.PI * 2)) * 255); + const b = Math.floor(Math.abs(Math.sin((hue + 0.66) * Math.PI * 2)) * 255); + + return (r << 16) | (g << 8) | b; + } + + // ======================================================================== + // Configuration and Control + // ======================================================================== + + setGridVisible(visible) { + this.showGrid = visible; + this.drawGrid(); + } + + setScrollSpeed(speed) { + this.scrollSpeed = Math.max(0.1, Math.min(10.0, speed)); + } + + setVerticalScale(scale) { + this.verticalScale = Math.max(0.2, Math.min(3.0, scale)); + } + + setTitle(title) { + this.title = title; + this.titleText.text = title; + } + + resize(x, y, width, height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.container.x = x; + this.container.y = y; + + this.draw(); + } + + // ======================================================================== + // Statistics and Debugging + // ======================================================================== + + getVertexCount() { + return this.lines.reduce((sum, line) => sum + line.points.length, 0); + } + + getLineCount() { + return this.lines.length; + } + + getOldestTimestamp() { + if (this.lines.length === 0) return null; + return Math.min(...this.lines.map(l => l.timestamp)); + } + + getNewestTimestamp() { + if (this.lines.length === 0) return null; + return Math.max(...this.lines.map(l => l.timestamp)); + } + + getStats() { + return { + lineCount: this.getLineCount(), + vertexCount: this.getVertexCount(), + oldestTimestamp: this.getOldestTimestamp(), + newestTimestamp: this.getNewestTimestamp(), + timeSpan: this.getNewestTimestamp() - this.getOldestTimestamp(), + }; + } +} |
