summaryrefslogtreecommitdiff
path: root/src/timeseries-plot.js
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2026-05-29 21:49:20 -0400
committergrothedev <grothedev@gmail.com>2026-05-29 21:49:20 -0400
commit6196004b51a6850909c154f5402ff4858eab479a (patch)
tree126b8bb1600d0a656e0df016e25d08c390f3540e /src/timeseries-plot.js
parent27dc5849c3eaf4824d79938e7077abdbe2c82e24 (diff)
mv web stuff to root project dirHEADprototypeframeworkmain
Diffstat (limited to 'src/timeseries-plot.js')
-rw-r--r--src/timeseries-plot.js277
1 files changed, 277 insertions, 0 deletions
diff --git a/src/timeseries-plot.js b/src/timeseries-plot.js
new file mode 100644
index 0000000..e35a704
--- /dev/null
+++ b/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(),
+ };
+ }
+}