at zoomed in level i render everything including the candle bodies
at medium zoomed out i just render the candle wicks not the bodies

when at high zoomed out i would like to optimize by just rendering a line chart. 


<!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>