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