I have some code.
Please remove the LOD optimization and simply use only the line chart optimization.
These optimizations are to deal with very high numbers of candlesticks

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Three.js Candlesticks with LOD Optimization</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 high = open + Math.random() * 10;
    const low = open - Math.random() * 10;
    const close = low + Math.random() * (high - low);
    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];
}

// Define colors in normalized RGB
const bullishColor = rgbToNormalized(0, 0.7 * 255, 0); // Green
const bearishColor = rgbToNormalized(0.7 * 255, 0, 0); // Red

// Generate data (Adjust the count to 5 million)
const ohlcData = generateOHLCData(5000000);

// Variables for candlestick rendering
let instancedMesh, wickLineSegments, aggregatedLine;
let instanceMatrices, instanceColors, wickVertices, wickColors;
let geometryBar, wickGeometry, aggregatedGeometry;
const maxVisibleCandlesticks = 2000; // Maximum number of visible candlesticks to render
const barWidth = 1;
const candlestickSpacing = barWidth * 1.5;
const totalCandlesticks = ohlcData.length;
const halfCandlesticks = totalCandlesticks / 2;

// Create instanced candlesticks with dynamic visibility
function createInstancedCandlesticks() {
  // Materials
  const material = new THREE.MeshBasicMaterial({ vertexColors: true });

  // Geometries for the candlestick body
  geometryBar = new THREE.BoxGeometry(barWidth, 1, 0);
  
  // Create instanced mesh with capacity for maximum visible candlesticks
  instancedMesh = new THREE.InstancedMesh(geometryBar, material, maxVisibleCandlesticks);
  instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);

  // Prepare instance matrices and colors
  instanceMatrices = new Float32Array(maxVisibleCandlesticks * 16); // 4x4 matrices
  instanceColors = new Float32Array(maxVisibleCandlesticks * 3); // RGB per instance

  // Set the per-instance color attribute on the geometry
  geometryBar.setAttribute('instanceColor', new THREE.InstancedBufferAttribute(instanceColors, 3));
  geometryBar.attributes.instanceColor.setUsage(THREE.DynamicDrawUsage);

  // Prepare wick geometry
  wickGeometry = new THREE.BufferGeometry();
  wickVertices = new Float32Array(maxVisibleCandlesticks * 6); // 2 points per wick, 3 components per point
  wickColors = new Float32Array(maxVisibleCandlesticks * 6); // 2 points per wick, 3 components per point
  wickGeometry.setAttribute('position', new THREE.BufferAttribute(wickVertices, 3));
  wickGeometry.setAttribute('color', new THREE.BufferAttribute(wickColors, 3)); // Add color to wicks
  const wickMaterial = new THREE.LineBasicMaterial({ vertexColors: true }); // Enable vertex colors
  wickLineSegments = new THREE.LineSegments(wickGeometry, wickMaterial);
  scene.add(wickLineSegments);

  // Add the instanced mesh to the scene
  scene.add(instancedMesh);

  // Prepare aggregated line geometry and material
  aggregatedGeometry = new THREE.BufferGeometry();
  const aggregatedMaterial = new THREE.LineBasicMaterial({ color: 0xCCCCCC }); // Light gray color
  aggregatedLine = new THREE.Line(aggregatedGeometry, aggregatedMaterial);
  scene.add(aggregatedLine);
}

// Create the candlesticks
createInstancedCandlesticks();

// Zoom logic
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 short delay
  visibilityUpdateTimeout = setTimeout(updateCandlestickVisibility, 50); // Adjust the delay as needed
}

// Function to update candlestick visibility based on zoom level
function updateCandlestickVisibility() {
  const totalCandlesticks = ohlcData.length;

  // Compute visible index range, adjusting for centering
  const firstVisibleIndex = Math.max(0, Math.floor((camera.left / candlestickSpacing) + halfCandlesticks));
  const lastVisibleIndex = Math.min(totalCandlesticks - 1, Math.ceil((camera.right / candlestickSpacing) + halfCandlesticks));

  let visibleCandlestickCount = lastVisibleIndex - firstVisibleIndex + 1;

  // Adjust the threshold for visibility based on zoom level
  const bodyThreshold = 700;
  const wickThreshold = 5000; // New wick threshold
  const aggregateThreshold = 10000; // Threshold to switch to aggregated representation

  if (visibleCandlestickCount > aggregateThreshold) {
    // Hide individual candlesticks and wicks
    instancedMesh.visible = false;
    wickLineSegments.visible = false;

    // Show aggregated line
    aggregatedLine.visible = true;

    // Generate aggregated data
    const maxPoints = 2000; // Limit the number of points in the aggregated line
    const step = Math.ceil(visibleCandlestickCount / maxPoints);

    const positions = [];
    for (let i = firstVisibleIndex; i <= lastVisibleIndex; i += step) {
      const index = i;
      const d = ohlcData[index];
      const currentX = (index - halfCandlesticks) * candlestickSpacing;
      const y = (d.high + d.low) / 2; // You can choose to plot close or an average
      positions.push(currentX, y, 0);
    }

    // Update aggregated line geometry
    const positionAttribute = new THREE.Float32BufferAttribute(positions, 3);
    aggregatedGeometry.setAttribute('position', positionAttribute);
    aggregatedGeometry.computeBoundingSphere();

  } else if (visibleCandlestickCount > wickThreshold) {
    // Hide candlestick bodies
    instancedMesh.visible = false;
    // Show wicks
    wickLineSegments.visible = true;
    // Hide aggregated line
    aggregatedLine.visible = false;

    // If visibleCandlestickCount is greater than maxVisibleCandlesticks, sample the data
    let step = 1;
    if (visibleCandlestickCount > maxVisibleCandlesticks) {
      step = Math.ceil(visibleCandlestickCount / maxVisibleCandlesticks);
    }

    let renderCount = 0;

    for (let i = firstVisibleIndex; i <= lastVisibleIndex; i += step) {
      const index = i;
      const d = ohlcData[index];
      const isBullish = d.close >= d.open;
      const currentX = (index - halfCandlesticks) * candlestickSpacing;

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

      // Wick colors
      const color = isBullish ? bullishColor : bearishColor;
      wickColors.set([...color, ...color], wickIndex);

      renderCount++;
      if (renderCount >= maxVisibleCandlesticks) break;
    }

    // Update wick geometry
    wickGeometry.setDrawRange(0, renderCount * 2); // 2 vertices per line
    wickGeometry.attributes.position.array.set(wickVertices.subarray(0, renderCount * 6));
    wickGeometry.attributes.position.needsUpdate = true;
    wickGeometry.attributes.color.array.set(wickColors.subarray(0, renderCount * 6));
    wickGeometry.attributes.color.needsUpdate = true;
    wickGeometry.computeBoundingSphere();

  } else {
    // Show candlestick bodies and wicks
    instancedMesh.visible = true;
    wickLineSegments.visible = true;
    // Hide aggregated line
    aggregatedLine.visible = false;

    // If visibleCandlestickCount is greater than maxVisibleCandlesticks, sample the data
    let step = 1;
    if (visibleCandlestickCount > maxVisibleCandlesticks) {
      step = Math.ceil(visibleCandlestickCount / maxVisibleCandlesticks);
    }

    let renderCount = 0;
    const dummy = new THREE.Object3D();

    for (let i = firstVisibleIndex; i <= lastVisibleIndex; i += step) {
      const index = i;
      const d = ohlcData[index];
      const isBullish = d.close >= d.open;
      const height = Math.abs(d.close - d.open);
      const y = (d.close + d.open) / 2;
      const currentX = (index - halfCandlesticks) * candlestickSpacing;

      // Set the position and scale for the body
      dummy.position.set(currentX, y, 0);
      dummy.scale.set(1, height, 1); // Width is set to 1; barWidth is already accounted for in scaling
      dummy.updateMatrix();

      // Copy the matrix to the instanceMatrices array
      dummy.matrix.toArray(instanceMatrices, renderCount * 16);

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

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

      // Wick colors
      wickColors.set([...color, ...color], wickIndex);

      renderCount++;
      if (renderCount >= maxVisibleCandlesticks) break;
    }

    // Update instancedMesh attributes
    instancedMesh.count = renderCount;

    // Update instance matrices and colors
    instancedMesh.instanceMatrix.array.set(instanceMatrices.subarray(0, renderCount * 16));
    instancedMesh.instanceMatrix.needsUpdate = true;

    geometryBar.attributes.instanceColor.array.set(instanceColors.subarray(0, renderCount * 3));
    geometryBar.attributes.instanceColor.needsUpdate = true;

    // Update wick geometry
    wickGeometry.setDrawRange(0, renderCount * 2); // 2 vertices per line
    wickGeometry.attributes.position.array.set(wickVertices.subarray(0, renderCount * 6));
    wickGeometry.attributes.position.needsUpdate = true;
    wickGeometry.attributes.color.array.set(wickColors.subarray(0, renderCount * 6));
    wickGeometry.attributes.color.needsUpdate = true;
    wickGeometry.computeBoundingSphere();
  }
}

// Initial visibility update
updateCandlestickVisibility();

// 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();
  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 to `distanceForGrab`
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
    clearTimeout(visibilityUpdateTimeout);
    visibilityUpdateTimeout = setTimeout(updateCandlestickVisibility, 50);
} else if (isDraggingEndpoint) {
    // Get current mouse position in normalized device coordinates (NDC)
    const rect = renderer.domElement.getBoundingClientRect();
    const currentMouse = new THREE.Vector2(
      ((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
    const previousMouseWorld = new THREE.Vector3(previousMousePosition.x, previousMousePosition.y, 0).unproject(camera);
    const currentMouseWorld = new THREE.Vector3(currentMouse.x, currentMouse.y, 0).unproject(camera);

    // Compute delta in world coordinates
    const deltaWorld = currentMouseWorld.clone().sub(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>