import { Container, Graphics, Text } from 'pixi.js'; /** * WaterfallGraph - A scrolling waterfall display * Starts simple with basic line rendering */ export class WaterfallGraph { constructor(config) { this.x = config.x; this.y = config.y; this.width = config.width; this.height = config.height; this.title = config.title; this.baseColor = config.color || 0xff6666; this.container = new Container(); this.container.x = this.x; this.container.y = this.y; // Graphics layers this.borderGraphics = new Graphics(); this.gridGraphics = new Graphics(); this.linesGraphics = new Graphics(); this.container.addChild(this.gridGraphics); this.container.addChild(this.linesGraphics); this.container.addChild(this.borderGraphics); // Title text 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); // Waterfall data this.lines = []; this.maxLines = 50; this.pointsPerLine = 100; this.frameCounter = 0; this.showGrid = true; // Time scaling and zoom this.scrollSpeed = 1.0; // Speed multiplier for scrolling this.baseScrollSpeed = 1.0; this.verticalScale = 1.0; // Vertical zoom: >1 = zoomed in (see less history), <1 = zoomed out (see more) this.draw(); } draw() { this.drawBorder(); this.drawGrid(); } 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 }); } } update(time, graphIdx) { this.frameCounter++; // Add new line every 10 frames if (this.frameCounter % 10 === 0 && this.lines.length < this.maxLines) { this.addLine(time, graphIdx); } // Scroll existing lines down this.scrollLines(); // Remove off-screen lines this.lines = this.lines.filter(line => line.yOffset < this.height + 50); // Redraw all lines this.drawLines(); } addLine(time, graphIdx) { const line = { points: [], yOffset: 0, color: this.generateColor(time), }; // Generate sine wave points const phase = time + (graphIdx * 2); const freq = 2.0 + Math.sin(time * 0.5 + graphIdx) * 1.0; for (let i = 0; i < this.pointsPerLine; i++) { const x = (i / this.pointsPerLine) * this.width; const normalizedX = (i / this.pointsPerLine) * 2 - 1; // -1 to 1 const y = Math.sin(i * 0.1 * freq + phase) * 30; // Amplitude in pixels line.points.push({ x, y }); } this.lines.push(line); } scrollLines() { const speed = this.baseScrollSpeed * this.scrollSpeed; this.lines.forEach(line => { line.yOffset += speed; }); } setScrollSpeed(speed) { // Clamp between 0.1 (slow) and 5.0 (fast) this.scrollSpeed = Math.max(0.1, Math.min(5.0, speed)); } getScrollSpeed() { return this.scrollSpeed; } setVerticalScale(scale) { // Clamp between 0.2 (zoomed out, see more history) and 3.0 (zoomed in, see less) this.verticalScale = Math.max(0.2, Math.min(3.0, scale)); } getVerticalScale() { return this.verticalScale; } drawLines() { this.linesGraphics.clear(); for (const line of this.lines) { if (line.points.length < 2) continue; // Apply vertical scale to y positions // Current time is at top (y=0), older data has larger yOffset 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; } setGridVisible(visible) { this.showGrid = visible; this.drawGrid(); } 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(); } getVertexCount() { return this.lines.reduce((sum, line) => sum + line.points.length, 0); } getLineCount() { return this.lines.length; } }