webgl-plot

repository·webglplot-v2·Indexed 20 days ago

https://github.com/danchitnis/webgl-plot

A high-performance 2D plotting library based on native WebGL designed for rendering multi-line plots of large static datasets and high-frequency dynamic data with minimal CPU overhead. It features support for native and thick lines, logarithmic scaling via setLogAxis(), and an optimized WebglLineRoll class for incremental streaming updates.

Tokens
23.3K
Snippets
67
Records
87
Agent score
68%

What's inside webgl-plot

  1. Understand Data Bounds and Coordinate Space Information

    webglplot-v2

    The webgl-plot API (v2) returns bounds objects that include coordinateSpace information. This eliminates ambiguity when determining if bounds are in linear or logarithmic space.

    getDataBounds() vs getAllDataBounds():

    • getAllDataBounds(): Returns the complete extent of all data. Use this to auto-scale to fit everything in view.
    • getDataBounds(): Returns the current viewport bounds. Use this to maintain the current zoom/pan state when data changes or when switching coordinate spaces.

    Return Object Shape:

    { 
      minX: number, 
      maxX: number, 
      minY: number, 
      maxY: number, 
      coordinateSpace: { x: "linear" | "log", y: "linear" | "log" } 
    }
  2. How UnifiedLinePlot determines rendering mode

    webglplot-v2

    The UnifiedLinePlot automatically switches between WebglLinePlot (thin lines) and WebglLineThick (thick lines).

    Critical Rule: The plotter type is determined by the thickness of the first line in the array passed to initLines().

    • If the first line's thickness <= 1.0: The plotter uses WebglLinePlot. All subsequent lines will have their thickness forced to 1.0.
    • If the first line's thickness > 1.0: The plotter uses WebglLineThick. Individual thickness values for all lines are preserved.

    To switch from a thin plotter to a thick plotter, you must re-initialize the plotter using initLines() with a line that has thickness > 1.0 at the start of the array.

    // Scenario A: Thin first → All lines become thin (thickness 1.0)
    plotter.initLines([
      { points: data1, thickness: 1.0, color: [1, 0, 0, 1] }, // ← Determines WebglLinePlot
      { points: data2, thickness: 5.0, color: [0, 1, 0, 1] }, // Forced to 1.0!
    ]);
    
    // Scenario B: Thick first → All lines can be thick
    plotter.initLines([
      { points: data1, thickness: 5.0, color: [1, 0, 0, 1] }, // ← Determines WebglLineThick
      { points: data2, thickness: 1.0, color: [0, 1, 0, 1] }, // Preserved as 1.0
    ]);
  3. How Line Roll works for streaming updates

    webglplot-v2

    The WebglLineRoll (v2) approach is optimized for streaming updates. Instead of re-uploading an entire line buffer every frame, it only uploads the new points being appended.

    This makes it significantly more efficient when you have many lines or a large history buffer but are only adding a small number of points per frame. The computational complexity per frame is proportional to $O(\text{lines} \times \text{pointsPerFrame})$ rather than $O(\text{lines} \times \text{bufferSize})$ used in older approaches.

  4. How user-managed WebGL context works in webgl-plot

    webglplot-v2

    Unlike many WebGL libraries, webgl-plot v2 requires you to manage the WebGL2 context directly. This gives you full control over WebGL state, easier integration with frameworks like React or Vue, and the ability to mix multiple plotter types on the same canvas.

    Responsibilities of the user:

    • Create the context: Use canvas.getContext('webgl2') or provided helpers.
    • Handle canvas clearing: Use gl.clear() or clearCanvas().
    • Manage the render loop: Use requestAnimationFrame() to trigger your draw calls.
  5. Choose between autoScale() and Coordinate Transformation

    webglplot-v2

    There are two distinct workflows for scaling data. Choosing the wrong one can lead to unexpected results.

    Option A: Simple Auto-Scaling (autoScale())

    Use autoScale() when you want to fit data within the currently active coordinate space.

    • When to use: Basic auto-scaling needs where you don't care about preserving a specific zoom/pan state.
    • Warning: Do not combine autoScale() with manual coordinate transforms like transformToLogSpace().

    Option B: Coordinate Transformation (transform*())

    Use getAllDataBounds() or getDataBounds() followed by transformToLogSpace() or transformToLinearSpace() when you need explicit control over coordinate conversion.

    • When to use: When you need to preserve current zoom/pan state during coordinate changes or when switching between linear and log systems.
    // Option A: Simple Auto-Scaling
    plotter.setLogAxis(false, true);
    plotter.autoScale();
    
    // Option B: Coordinate Transformation (Manual Control)
    plotter.setLogAxis(false, true);
    const bounds = plotter.getAllDataBounds();
    if (bounds) {
      plotter.transformToLogSpace(bounds);
    }
  6. Smart Filtering for Logarithmic Axes

    webglplot-v2

    When using logarithmic axes, the library automatically filters lines that are incompatible with log scaling (i.e., contain non-positive values).

    Filtering Rules:

    • Included: Lines with $\ge 10%$ positive values AND $\ge 2$ valid points.
    • Excluded: Lines with $< 10%$ positive values OR $< 2$ valid points.

    Behavior: Excluded lines may still render (their positive portions will be visible), but they are ignored during the calculation of data bounds for scaling purposes.

  7. How Logarithmic Scaling works in webgl-plot

    webglplot-v2

    Logarithmic scaling is implemented using GPU-accelerated transformations for high performance:

    1. GPU Transform: The log10(value) calculation is performed in vertex shaders, enabling real-time performance.
    2. Smart Filtering: Negative or zero values are automatically moved off-screen because $\log_{10}(x)$ is undefined for $x \le 0$.
    3. Auto-scaling Intelligence: To prevent issues with sparse data, lines containing less than 10% positive values are excluded from bounds calculations.
    4. Zoom Independence: Scaling is designed to work correctly regardless of the current zoom level.

    This approach is ideal for visualizing exponential data, power laws, and scientific measurements spanning multiple orders of magnitude.

  8. Compare Native vs Thick Lines

    webglplot-v2

    The library supports two types of lines:

    • Native (Thin) Lines: High performance, uses native WebGL line drawing.
    • Thick Lines: Provides better visibility for highlighting specific lines, but is approximately 6 times slower due to the computation required for line data points. Use them sparingly.
  9. Integrate webgl-plot with React

    webglplot-v2

    When using React, manage the WebGL lifecycle within a useEffect hook.

    1. Use useRef for the canvas, WebGL context, and plotter instance.
    2. Initialize the plotter and start the requestAnimationFrame loop inside useEffect.
    3. Cleanup: In the effect's return function, cancel the animation frame and call plotter.cleanup() to release WebGL resources.
    import React, { useEffect, useRef } from "react";
    import {
      clearCanvas,
      handleCanvasResize,
      setupCanvasAndWebGL,
      UnifiedLinePlot,
    } from "webgl-plot";
    
    const PlotComponent: React.FC = () => {
      const canvasRef = useRef<HTMLCanvasElement>(null);
      const glRef = useRef<WebGL2RenderingContext | null>(null);
      const plotterRef = useRef<UnifiedLinePlot | null>(null);
      const animationRef = useRef<number>();
    
      useEffect(() => {
        if (!canvasRef.current) return;
    
        // Setup once
        const gl = setupCanvasAndWebGL(canvasRef.current, {
          backgroundColor: [0.1, 0.1, 0.2, 1],
        });
        glRef.current = gl;
    
        const plotter = new UnifiedLinePlot(gl, 5);
        plotterRef.current = plotter;
    
        plotter.initLines([
          {
            points: new Float32Array([0, 0, 1, 1, 2, 0.5]),
            color: [1, 0, 0, 1],
            thickness: 2.0,
            enabled: true,
          },
        ]);
    
        // Render loop
        const render = () => {
          if (gl && plotter) {
            clearCanvas(gl);
            plotter.draw();
            animationRef.current = requestAnimationFrame(render);
          }
        };
        animationRef.current = requestAnimationFrame(render);
    
        // Cleanup
        return () => {
          if (animationRef.current) {
            cancelAnimationFrame(animationRef.current);
          }
          plotter?.cleanup();
        };
      }, []);
    
      // Handle canvas resize
      useEffect(() => {
        const handleResize = () => {
          if (canvasRef.current && glRef.current) {
            handleCanvasResize(canvasRef.current, glRef.current);
          }
        };
    
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
      }, []);
    
      return <canvas ref={canvasRef} style={{ width: "100%", height: "400px" }} />;
    };
  10. Use webgl-plot via CDN (UMD and ESM)

    webglplot-v2

    For testing or small projects without a build step, you can use pre-bundled versions via CDN.

    UMD Bundle: Import the script directly in your HTML file.

    ESM Module: Use a <script type="module"> and import from the ESM CDN URL.

    <!-- UMD Bundle -->
    <script src="https://cdn.jsdelivr.net/gh/danchitnis/webgl-plot@master/dist/webglplot.umd.min.js"></script>
    
    <!-- ESM Module -->
    <script type="module" src="your-code.js"></script>
    // inside your-code.js
    import {
      WebglPlot,
      WebglLine,
      ColorRGBA,
    } from "https://cdn.jsdelivr.net/gh/danchitnis/webgl-plot@master/dist/webglplot.esm.min.js";
  11. Use WebglLineRoll for incremental streaming

    webglplot-v2

    To implement high-performance streaming of multiple lines, use the WebglLineRoll class. You initialize it with a bufferSize (the maximum history per line) and the numLines to be tracked.

    1. Initialize: Create the WebglLineRoll instance with your WebGL context, buffer size, and line count.
    2. Configure Colors: Use setLineColor(color, lineIndex) to assign a ColorRGBA to specific lines.
    3. Prepare Payload: Create a payload structure (typically a 2D array number[][]) where payload[lineIndex] contains the new points for that line.
    4. Update and Draw: In your animation loop, update the payload with new samples and call roll.addPoints(payload) followed by roll.draw().
    import { setupCanvasAndWebGL, WebglLineRoll, ColorRGBA } from "webgl-plot";
    
    const canvas = document.getElementById("my_canvas") as HTMLCanvasElement;
    const gl = setupCanvasAndWebGL(canvas, {
      backgroundColor: [0, 0, 0, 1],
      antialias: false,
      powerPerformance: "high-performance",
    });
    
    const bufferSize = 8192;
    const numLines = 200;
    const pointsPerFrame = 1;
    
    const roll = new WebglLineRoll(gl, bufferSize, numLines);
    for (let i = 0; i < numLines; i++) {
      roll.setLineColor(new ColorRGBA(255, 255, 255, 1), i);
    }
    
    // Payload shape: [lineIndex][pointIndex]
    const payload: number[][] = Array.from({ length: numLines }, () =>
      Array(pointsPerFrame).fill(0)
    );
    
    function frame() {
      // Fill only the *new* samples for this frame.
      for (let i = 0; i < numLines; i++) {
        payload[i][0] = Math.sin(performance.now() * 0.002 + i * 0.1) * 0.6;
      }
    
      roll.addPoints(payload);
      gl.clear(gl.COLOR_BUFFER_BIT);
      roll.draw();
    
      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
  12. Quick Start with Logarithmic Axes

    webglplot-v2

    To use logarithmic scaling in webgl-plot, initialize a UnifiedLinePlot, enable the desired log axes using setLogAxis, and then apply a transformation to the data bounds to ensure the view is correctly scaled for the new coordinate space.

    Workflow:

    1. Initialize UnifiedLinePlot with a WebGL context.
    2. Call setLogAxis(x, y) to enable log-10 scaling.
    3. Retrieve bounds using getAllDataBounds() (to fit all data) or getDataBounds() (to preserve current zoom/pan).
    4. Apply transformToLogSpace(bounds) to handle the coordinate conversion.
    5. Run the render loop using requestAnimationFrame.
    import { setupCanvasAndWebGL, UnifiedLinePlot, clearCanvas } from "webgl-plot";
    
    // Setup canvas and WebGL context
    const canvas = document.getElementById("canvas") as HTMLCanvasElement;
    const gl = setupCanvasAndWebGL(canvas, {
      backgroundColor: [0, 0, 0, 1],
    });
    
    // Create unified line plotter
    const plotter = new UnifiedLinePlot(gl, 1);
    plotter.initLines([
      {
        points: yourData, // Float32Array of [x1,y1,x2,y2,...]
        color: [1, 0, 0, 1],
        thickness: 2,
        enabled: true,
      },
    ]);
    
    // Enable logarithmic Y-axis and scale appropriately
    plotter.setLogAxis(false, true);
    const bounds = plotter.getAllDataBounds(); 
    if (bounds) {
      plotter.transformToLogSpace(bounds);
    }
    
    // Render loop
    function animate() {
      clearCanvas(gl);
      plotter.draw();
      requestAnimationFrame(animate);
    }
    animate();