Загрузка данных


import type { Anchor, Bounds, ContainerSize, Point, SeriesApi } from './types';

import type { Coordinate, IChartApi, Logical, Time } from 'lightweight-charts';

interface SeriesTimePoint {
  time: number;
  logical: number;
}

export function getPriceFromYCoordinate(series: SeriesApi, yCoordinate: number): number | null {
  return series.coordinateToPrice(yCoordinate as Coordinate);
}

export function getYCoordinateFromPrice(series: SeriesApi, price: number): Coordinate | null {
  return series.priceToCoordinate(price);
}

export function getTimeFromXCoordinate(chart: IChartApi, xCoordinate: number, series?: SeriesApi): Time | null {
  const time = chart.timeScale().coordinateToTime(xCoordinate as Coordinate);

  if (time !== null) {
    return time;
  }

  if (!series) {
    return null;
  }

  const logical = chart.timeScale().coordinateToLogical(xCoordinate as Coordinate);

  if (logical === null) {
    return null;
  }

  return getTimeFromLogical(series, Number(logical));
}

export function getXCoordinateFromTime(chart: IChartApi, time: Time, series?: SeriesApi): Coordinate | null {
  const coordinate = chart.timeScale().timeToCoordinate(time);

  if (coordinate !== null) {
    return coordinate;
  }

  if (!series || typeof time !== 'number') {
    return null;
  }

  const logical = getLogicalFromTime(series, time);

  if (logical === null) {
    return null;
  }

  return chart.timeScale().logicalToCoordinate(logical as Logical);
}

export function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(value, max));
}

export function getContainerSize(container: HTMLElement): ContainerSize {
  const rect = container.getBoundingClientRect();

  return {
    width: rect.width,
    height: rect.height,
  };
}

export function clampPointToContainer(point: Point, container: HTMLElement): Point {
  const { width, height } = getContainerSize(container);

  return {
    x: clamp(point.x, 0, width),
    y: clamp(point.y, 0, height),
  };
}

export function getPointerPoint(container: HTMLElement, event: PointerEvent): Point {
  const rect = container.getBoundingClientRect();

  return clampPointToContainer(
    {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    },
    container,
  );
}

export function isNearPoint(point: Point, x: number, y: number, tolerance: number): boolean {
  return Math.abs(point.x - x) <= tolerance && Math.abs(point.y - y) <= tolerance;
}

export function isPointInBounds(point: Point, bounds: Bounds, tolerance = 0): boolean {
  return (
    point.x >= bounds.left - tolerance &&
    point.x <= bounds.right + tolerance &&
    point.y >= bounds.top - tolerance &&
    point.y <= bounds.bottom + tolerance
  );
}

export function normalizeBounds(
  left: number,
  right: number,
  top: number,
  bottom: number,
  container: HTMLElement,
): Bounds {
  const { width, height } = getContainerSize(container);

  return {
    left: clamp(Math.min(left, right), 0, width),
    right: clamp(Math.max(left, right), 0, width),
    top: clamp(Math.min(top, bottom), 0, height),
    bottom: clamp(Math.max(top, bottom), 0, height),
  };
}

export function shiftTimeByPixels(chart: IChartApi, time: Time, offsetX: number, series?: SeriesApi): Time | null {
  const coordinate = getXCoordinateFromTime(chart, time, series);

  if (coordinate === null) {
    return null;
  }

  return getTimeFromXCoordinate(chart, Number(coordinate) + offsetX, series);
}

export function getPriceDelta(series: SeriesApi, fromY: number, toY: number): number {
  const fromPrice = getPriceFromYCoordinate(series, fromY);
  const toPrice = getPriceFromYCoordinate(series, toY);

  if (fromPrice === null || toPrice === null) {
    return 0;
  }

  return toPrice - fromPrice;
}

export function getPriceRangeInContainer(
  series: SeriesApi,
  container: HTMLElement,
): { min: number; max: number } | null {
  const { height } = getContainerSize(container);

  if (!height) {
    return null;
  }

  const topPrice = getPriceFromYCoordinate(series, 0);
  const bottomPrice = getPriceFromYCoordinate(series, height);

  if (topPrice === null || bottomPrice === null) {
    return null;
  }

  return {
    min: Math.min(topPrice, bottomPrice),
    max: Math.max(topPrice, bottomPrice),
  };
}

export function getAnchorFromPoint(chart: IChartApi, series: SeriesApi, point: Point): Anchor | null {
  const time = getTimeFromXCoordinate(chart, point.x, series);
  const price = getPriceFromYCoordinate(series, point.y);

  if (time === null || price === null) {
    return null;
  }

  return {
    time,
    price,
  };
}

function getNumericSeriesTimePoints(series: SeriesApi): SeriesTimePoint[] {
  return series.data().reduce<SeriesTimePoint[]>((points, item, logical) => {
    if (typeof item.time !== 'number') {
      return points;
    }

    points.push({
      time: item.time,
      logical,
    });

    return points;
  }, []);
}

function getLogicalFromTime(series: SeriesApi, time: number): number | null {
  const points = getNumericSeriesTimePoints(series);

  if (!points.length) {
    return null;
  }

  if (points.length === 1) {
    return points[0].logical;
  }

  const { leftPoint, rightPoint } = getTimePointsForInterpolation(points, time);
  const timeRange = rightPoint.time - leftPoint.time;

  if (timeRange === 0) {
    return leftPoint.logical;
  }

  const ratio = (time - leftPoint.time) / timeRange;

  return leftPoint.logical + (rightPoint.logical - leftPoint.logical) * ratio;
}

function getTimeFromLogical(series: SeriesApi, logical: number): Time | null {
  const points = getNumericSeriesTimePoints(series);

  if (!points.length) {
    return null;
  }

  if (points.length === 1) {
    return points[0].time as Time;
  }

  const leftIndex = clamp(Math.floor(logical), 0, points.length - 2);
  const leftPoint = points[leftIndex];
  const rightPoint = points[leftIndex + 1];

  const logicalRange = rightPoint.logical - leftPoint.logical;

  if (logicalRange === 0) {
    return leftPoint.time as Time;
  }

  const ratio = (logical - leftPoint.logical) / logicalRange;
  const time = leftPoint.time + (rightPoint.time - leftPoint.time) * ratio;

  return Math.round(time) as Time;
}

function getTimePointsForInterpolation(
  points: SeriesTimePoint[],
  time: number,
): { leftPoint: SeriesTimePoint; rightPoint: SeriesTimePoint } {
  if (time <= points[0].time) {
    return {
      leftPoint: points[0],
      rightPoint: points[1],
    };
  }

  const lastIndex = points.length - 1;

  if (time >= points[lastIndex].time) {
    return {
      leftPoint: points[lastIndex - 1],
      rightPoint: points[lastIndex],
    };
  }

  let left = 0;
  let right = lastIndex;

  while (left <= right) {
    const middle = Math.floor((left + right) / 2);
    const middleTime = points[middle].time;

    if (middleTime === time) {
      return {
        leftPoint: points[middle],
        rightPoint: points[middle],
      };
    }

    if (middleTime < time) {
      left = middle + 1;
    } else {
      right = middle - 1;
    }
  }

  return {
    leftPoint: points[right],
    rightPoint: points[left],
  };
}