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


import type {
  LaidOutPriceAxisLabel,
  MeasuredPriceAxisLabel,
  PriceAxisLabelsLayoutOptions,
} from './types';

const DEFAULT_GAP = 2;

function compareLabels(left: MeasuredPriceAxisLabel, right: MeasuredPriceAxisLabel): number {
  if (left.desiredCoordinate !== right.desiredCoordinate) {
    return left.desiredCoordinate - right.desiredCoordinate;
  }

  if (left.priority !== right.priority) {
    return right.priority - left.priority;
  }

  return left.id.localeCompare(right.id);
}

function getCoordinate(
  desiredCoordinate: number,
  height: number,
  minCoordinate: number,
  maxCoordinate: number,
): number {
  const minimum = minCoordinate + height / 2;
  const maximum = maxCoordinate - height / 2;

  if (maximum < minimum) {
    return (minCoordinate + maxCoordinate) / 2;
  }

  return Math.min(Math.max(desiredCoordinate, minimum), maximum);
}

function distributeLabels(
  labels: readonly MeasuredPriceAxisLabel[],
  minCoordinate: number,
  maxCoordinate: number,
): LaidOutPriceAxisLabel[] {
  if (labels.length === 0) {
    return [];
  }

  if (labels.length === 1) {
    const label = labels[0];

    return [
      {
        ...label,
        coordinate: getCoordinate(label.desiredCoordinate, label.height, minCoordinate, maxCoordinate),
      },
    ];
  }

  const firstCoordinate = minCoordinate + labels[0].height / 2;
  const lastCoordinate = maxCoordinate - labels[labels.length - 1].height / 2;

  if (lastCoordinate <= firstCoordinate) {
    const coordinate = (minCoordinate + maxCoordinate) / 2;

    return labels.map((label) => ({
      ...label,
      coordinate,
    }));
  }

  const step = (lastCoordinate - firstCoordinate) / (labels.length - 1);

  return labels.map((label, index) => ({
    ...label,
    coordinate: firstCoordinate + step * index,
  }));
}

function layoutRange(
  labels: readonly MeasuredPriceAxisLabel[],
  minCoordinate: number,
  maxCoordinate: number,
  gap: number,
): LaidOutPriceAxisLabel[] {
  const sortedLabels = [...labels].sort(compareLabels);

  if (sortedLabels.length === 0) {
    return [];
  }

  const availableHeight = Math.max(0, maxCoordinate - minCoordinate);
  const labelsHeight = sortedLabels.reduce((total, label) => total + label.height, 0);
  const requiredHeight = labelsHeight + gap * Math.max(0, sortedLabels.length - 1);

  if (requiredHeight > availableHeight) {
    return distributeLabels(sortedLabels, minCoordinate, maxCoordinate);
  }

  const result = sortedLabels.map((label) => ({
    ...label,
    coordinate: getCoordinate(label.desiredCoordinate, label.height, minCoordinate, maxCoordinate),
  }));

  for (let index = 1; index < result.length; index += 1) {
    const previousLabel = result[index - 1];
    const currentLabel = result[index];
    const minimumCoordinate =
      previousLabel.coordinate + previousLabel.height / 2 + currentLabel.height / 2 + gap;

    currentLabel.coordinate = Math.max(currentLabel.coordinate, minimumCoordinate);
  }

  const lastLabel = result[result.length - 1];
  const maximumLastCoordinate = maxCoordinate - lastLabel.height / 2;

  if (lastLabel.coordinate > maximumLastCoordinate) {
    const offset = lastLabel.coordinate - maximumLastCoordinate;

    result.forEach((label) => {
      label.coordinate -= offset;
    });
  }

  for (let index = result.length - 2; index >= 0; index -= 1) {
    const currentLabel = result[index];
    const nextLabel = result[index + 1];
    const maximumCoordinate =
      nextLabel.coordinate - nextLabel.height / 2 - currentLabel.height / 2 - gap;

    currentLabel.coordinate = Math.min(currentLabel.coordinate, maximumCoordinate);
  }

  const firstLabel = result[0];
  const minimumFirstCoordinate = minCoordinate + firstLabel.height / 2;

  if (firstLabel.coordinate < minimumFirstCoordinate) {
    const offset = minimumFirstCoordinate - firstLabel.coordinate;

    result.forEach((label) => {
      label.coordinate += offset;
    });
  }

  return result;
}

export function layoutPriceAxisLabels(
  labels: readonly MeasuredPriceAxisLabel[],
  axisHeight: number,
  options: PriceAxisLabelsLayoutOptions = {},
): LaidOutPriceAxisLabel[] {
  if (axisHeight <= 0 || labels.length === 0) {
    return [];
  }

  const gap = options.gap ?? DEFAULT_GAP;
  const labelsWithValidCoordinate = labels.filter((label) =>
    Number.isFinite(label.desiredCoordinate),
  );

  if (options.reservedCoordinate === undefined || options.reservedHeight === undefined) {
    return layoutRange(labelsWithValidCoordinate, 0, axisHeight, gap);
  }

  const reservedCoordinate = Math.min(Math.max(options.reservedCoordinate, 0), axisHeight);
  const reservedHeight = Math.max(0, options.reservedHeight);
  const reservedTop = Math.max(0, reservedCoordinate - reservedHeight / 2);
  const reservedBottom = Math.min(axisHeight, reservedCoordinate + reservedHeight / 2);

  const labelsAbove = labelsWithValidCoordinate.filter(
    (label) => label.desiredCoordinate <= reservedCoordinate,
  );

  const labelsBelow = labelsWithValidCoordinate.filter(
    (label) => label.desiredCoordinate > reservedCoordinate,
  );

  const result = [
    ...layoutRange(labelsAbove, 0, Math.max(0, reservedTop - gap), gap),
    ...layoutRange(labelsBelow, Math.min(axisHeight, reservedBottom + gap), axisHeight, gap),
  ];

  return result.sort((left, right) => {
    if (left.priority !== right.priority) {
      return left.priority - right.priority;
    }

    return left.id.localeCompare(right.id);
  });
}