i want a line chart optimization for when the user is zoomed out far because when even if im just rendering the candle wicks it is still very laggy when multiple million are rendered at once.
It is still vital to see all of the detail of the price action


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Three.js Interactive Line with Candlesticks - Corrected Version</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="module">

import * as THREE from './three.module.js';

// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);

// Camera setup
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraSize = 200;
const camera = new THREE.OrthographicCamera(
  -cameraSize * aspectRatio,
  cameraSize * aspectRatio,
  cameraSize,
  -cameraSize,
  1,
  1000
);
camera.position.set(0, 0, 500);

// Renderer setup
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Function to generate random OHLC data
function generateOHLCData(count) {
  const data = new Array(count);
  let open = 50; // Starting price
  for (let i = 0; i < count; i++) {
    const delta = Math.random() * 20 - 10; // Random movement between -10 and +10
    const close = open + delta;
    const high = Math.max(open, close) + Math.random() * 5; // Random overshoot up to 5
    const low = Math.min(open, close) - Math.random() * 5; // Random undershoot up to 5
    data[i] = { open, high, low, close };
    open = close; // Next open starts where the last close was
  }
  return data;
}

// Convert RGB to normalized color values (0 to 1)
function rgbToNormalized(r, g, b) {
  return [r / 255, g / 255, b / 255];
}

// Arrays to store the instanced meshes and wicks
const instancedMeshes = [];
const wickLineSegmentsArray = [];

// Create instanced candlesticks with chunking
function createInstancedCandlesticks(data, chunkSize) {
  const barWidth = 1;

  // Materials
  const material = new THREE.MeshBasicMaterial({
    color: 0xffffff, // Set base color to white
    vertexColors: THREE.InstanceColor // Use per-instance colors
  });

  const wickMaterial = new THREE.LineBasicMaterial({ vertexColors: true }); // Enable vertex colors

  // Geometry for the candlestick body
  const geometryBar = new THREE.BoxGeometry(barWidth, 1, 0); // Width, height, depth

  // Define colors in normalized RGB
  const bullishColor = rgbToNormalized(0, 70, 0); // Custom RGB for bullish candles (dark green)
  const bearishColor = rgbToNormalized(138, 0, 0); // Custom RGB for bearish candles (dark red)

  const dataLength = data.length;
  const numChunks = Math.ceil(dataLength / chunkSize);

  let currentX = 0;

  const dummy = new THREE.Object3D();

  for (let chunkIndex = 0; chunkIndex < numChunks; chunkIndex++) {
    const startIndex = chunkIndex * chunkSize;
    const endIndex = Math.min(startIndex + chunkSize, dataLength);
    const chunkData = data.slice(startIndex, endIndex);

    const maxCandlesticks = chunkData.length;

    // Create instanced mesh for candlesticks
    const instancedMesh = new THREE.InstancedMesh(geometryBar, material, maxCandlesticks);

    const wickVertices = new Float32Array(maxCandlesticks * 6); // 2 points per wick, x,y,z per point
    const wickColors = new Float32Array(maxCandlesticks * 6); // Color per vertex
    const colorArray = new Float32Array(maxCandlesticks * 3); // Color per instance

    for (let index = 0; index < maxCandlesticks; index++) {
      const d = chunkData[index];
      const isBullish = d.close >= d.open;
      const height = Math.abs(d.close - d.open);
      const y = (d.close + d.open) / 2;

      // Set the position and scale for the body
      dummy.position.set(currentX, y, 0);
      dummy.scale.set(1, height, 1);
      dummy.updateMatrix();

      instancedMesh.setMatrixAt(index, dummy.matrix);

      // Set color for each instance
      const color = isBullish ? bullishColor : bearishColor;
      colorArray.set(color, index * 3);

      // Wick positions
      const wickIndex = index * 6;
      wickVertices[wickIndex] = currentX;
      wickVertices[wickIndex + 1] = d.low;
      wickVertices[wickIndex + 2] = 0;
      wickVertices[wickIndex + 3] = currentX;
      wickVertices[wickIndex + 4] = d.high;
      wickVertices[wickIndex + 5] = 0;

      // Wick colors (same color for both points)
      wickColors.set([...color, ...color], wickIndex);

      // Increment x for the next candlestick
      currentX += barWidth * 1.5;
    }

    // Assign the instance colors to the instanced mesh
    instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(colorArray, 3);
    // instancedMesh.instanceColor.needsUpdate = true; // Not strictly necessary here

    // Add the instances to the scene
    scene.add(instancedMesh);
    instancedMeshes.push(instancedMesh);

    // Create and add wick line segments to the scene
    const wickGeometry = new THREE.BufferGeometry();
    wickGeometry.setAttribute('position', new THREE.BufferAttribute(wickVertices, 3));
    wickGeometry.setAttribute('color', new THREE.BufferAttribute(wickColors, 3)); // Add color to wicks
    const wickLineSegments = new THREE.LineSegments(wickGeometry, wickMaterial);
    scene.add(wickLineSegments);
    wickLineSegmentsArray.push(wickLineSegments);
  }
}

// Generate data and create instanced candlesticks
const ohlcData = generateOHLCData(5000000); // Adjust the number as needed
createInstancedCandlesticks(ohlcData, 5000); // Chunk size can be adjusted

// Zoom logic (based on mouse wheel and key modifiers)
const zoomSpeed = 0.8;
let visibilityUpdateTimeout; // Timeout variable for visibility updates

function onWheel(event) {
  event.preventDefault();

  const rect = renderer.domElement.getBoundingClientRect();
  const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;

  const mouseWorld = new THREE.Vector3(mouseX, mouseY, 0).unproject(camera);
  const zoomFactor = 1 - event.deltaY * 0.001 * zoomSpeed;

  const ctrlPressed = event.ctrlKey;
  const shiftPressed = event.shiftKey;

  if (ctrlPressed) {
    camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
  } else if (shiftPressed) {
    camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
    camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
  } else {
    camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
    camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
  }

  camera.updateProjectionMatrix();

  // Clear previous timeout if it exists
  clearTimeout(visibilityUpdateTimeout);

  // Set a new timeout to update visibility after a delay
  visibilityUpdateTimeout = setTimeout(updateCandlestickVisibility, 100); // Adjust the delay as needed
}

// Function to update candlestick visibility based on zoom level
function updateCandlestickVisibility() {
  const visibleWidth = camera.right - camera.left;
  const candlestickWidth = 1.5; // Width of each candlestick including spacing
  const visibleCandlesticks = visibleWidth / candlestickWidth;

  // Adjust the threshold for visibility based on zoom level
  const threshold = 1000;

  const bodiesVisible = visibleCandlesticks <= threshold;

  for (const mesh of instancedMeshes) {
    mesh.visible = bodiesVisible;
  }

  for (const wick of wickLineSegmentsArray) {
    wick.visible = true; // Always show wicks
  }
}

// Prevent default browser behavior for wheel scrolling with Control or Shift key
window.addEventListener('wheel', onWheel, { passive: false });

// Prevent default context menu on right-click
window.addEventListener('contextmenu', (event) => {
  event.preventDefault();
});

// Render loop
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

// Handle window resize
function onWindowResize() {
  const aspect = window.innerWidth / window.innerHeight;
  camera.left = -cameraSize * aspect;
  camera.right = cameraSize * aspect;
  camera.top = cameraSize;
  camera.bottom = -cameraSize;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  updateCandlestickVisibility();
}
window.addEventListener('resize', onWindowResize);

// Start animation
animate();

// Panning functionality
let isPanning = false; // Track panning state
let isDraggingEndpoint = false; // Track endpoint dragging state
let selectedEndpointIndex = null; // Index of the selected endpoint (0 or 1)
const previousMousePosition = new THREE.Vector2(); // Store previous mouse position

// Raycaster for detecting line interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// **Interactive line code**
// Define line geometry and material
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(-50, 0, 0),
  new THREE.Vector3(50, 0, 0)
]);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.frustumCulled = false; // Prevent the line from being culled
scene.add(line);

// Set the distance for grabbing the line (in world units)
let distanceForGrab = 2; // You can adjust this value to make grabbing easier

// Set the raycaster's line precision
raycaster.params.Line.threshold = distanceForGrab; // For Three.js r127 and above

// Predefine vectors to avoid excessive allocations
const mouseWorld = new THREE.Vector3();
const deltaWorld = new THREE.Vector3();
const currentMouse = new THREE.Vector2();
const previousMouseWorld = new THREE.Vector3();

// Mouse down event
function onMouseDown(event) {
  if (event.button === 2) { // Right mouse button for panning
    isPanning = true;
    previousMousePosition.set(event.clientX, event.clientY);
  } else if (event.button === 0) { // Left mouse button for line interaction
    // Prepare raycaster
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = - ((event.clientY - rect.top) / rect.height) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    // Check intersection with the line
    const intersects = raycaster.intersectObject(line, true);

    if (intersects.length > 0) {
      // Clicked on the line
      // Determine which endpoint is closer to the mouse
      const positions = lineGeometry.attributes.position.array;
      const lineStart = new THREE.Vector3(positions[0], positions[1], positions[2]);
      const lineEnd = new THREE.Vector3(positions[3], positions[4], positions[5]);

      mouseWorld.copy(intersects[0].point);

      const distanceToStart = mouseWorld.distanceTo(lineStart);
      const distanceToEnd = mouseWorld.distanceTo(lineEnd);

      selectedEndpointIndex = (distanceToStart < distanceToEnd) ? 0 : 1;

      isDraggingEndpoint = true;
      // Set previous mouse position in NDC
      previousMousePosition.set(mouse.x, mouse.y);
    }
  }
}

// Mouse move event
function onMouseMove(event) {
  if (isPanning) {
    // Panning code
    const deltaX = event.clientX - previousMousePosition.x;
    const deltaY = event.clientY - previousMousePosition.y;

    const zoomFactorX = camera.right - camera.left;
    const zoomFactorY = camera.top - camera.bottom;

    const panX = (deltaX / window.innerWidth) * zoomFactorX;
    const panY = (deltaY / window.innerHeight) * zoomFactorY;

    camera.left -= panX;
    camera.right -= panX;
    camera.top += panY;
    camera.bottom += panY;
    camera.updateProjectionMatrix();

    previousMousePosition.set(event.clientX, event.clientY); // Update previous position

    // Update candlestick visibility after panning
    updateCandlestickVisibility();
  } else if (isDraggingEndpoint) {
    // Get current mouse position in normalized device coordinates (NDC)
    const rect = renderer.domElement.getBoundingClientRect();
    currentMouse.set(
      ((event.clientX - rect.left) / rect.width) * 2 - 1,
      - ((event.clientY - rect.top) / rect.height) * 2 + 1
    );

    // Convert previous and current mouse positions to world coordinates
    previousMouseWorld.set(previousMousePosition.x, previousMousePosition.y, 0).unproject(camera);
    mouseWorld.set(currentMouse.x, currentMouse.y, 0).unproject(camera);

    // Compute delta in world coordinates
    deltaWorld.subVectors(mouseWorld, previousMouseWorld);

    // Update the selected endpoint
    const positions = lineGeometry.attributes.position.array;
    const index = selectedEndpointIndex * 3; // 0 or 3

    positions[index] += deltaWorld.x;
    positions[index + 1] += deltaWorld.y;

    lineGeometry.attributes.position.needsUpdate = true;
    lineGeometry.computeBoundingSphere(); // Update bounding sphere for frustum culling and raycasting

    previousMousePosition.copy(currentMouse); // Update previous mouse position
  }
}

// Mouse up event
function onMouseUp(event) {
  if (event.button === 2) { // Right mouse button
    isPanning = false;
  } else if (event.button === 0) { // Left mouse button
    isDraggingEndpoint = false;
  }
}

// Add event listeners for mouse actions
window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);

  </script>
</body>
</html>