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


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

type StackAlignment = 'start' | 'center' | 'end';

const DEFAULT_GAP = 3;

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 createLabel(
  label: MeasuredPriceAxisLabel,
  coordinate: number,
): LaidOutPriceAxisLabel {
  return {
    ...label,
    coordinate,
  };
}

function getStackHeight(
  labels: readonly MeasuredPriceAxisLabel[],
  gap: number,
): number {
  const labelsHeight = labels.reduce(
    (height, label) => height + label.height,
    0,
  );

  return labelsHeight + gap * Math.max(0, labels.length - 1);
}

function placeStack(
  labels: readonly MeasuredPriceAxisLabel[],
  top: number,
  gap: number,
): LaidOutPriceAxisLabel[] {
  let currentTop = top;

  return labels.map((label) => {
    const coordinate = currentTop + label.height / 2;

    currentTop += label.height + gap;

    return createLabel(label, coordinate);
  });
}

function placeOverflowingStack(
  labels: readonly MeasuredPriceAxisLabel[],
  minCoordinate: number,
  maxCoordinate: number,
  gap: number,
  alignment: StackAlignment,
): LaidOutPriceAxisLabel[] {
  const stackHeight = getStackHeight(labels, gap);
  const availableHeight = Math.max(0, maxCoordinate - minCoordinate);

  let top = minCoordinate;

  if (alignment === 'end') {
    top = maxCoordinate - stackHeight;
  }

  if (alignment === 'center') {
    top = minCoordinate + (availableHeight - stackHeight) / 2;
  }

  return placeStack(labels, top, gap);
}

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

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

  const availableHeight = Math.max(0, maxCoordinate - minCoordinate);
  const stackHeight = getStackHeight(sortedLabels, gap);

  if (stackHeight > availableHeight) {
    return placeOverflowingStack(
      sortedLabels,
      minCoordinate,
      maxCoordinate,
      gap,
      overflowAlignment,
    );
  }

  const result = sortedLabels.map((label) => {
    const minimum = minCoordinate + label.height / 2;
    const maximum = maxCoordinate - label.height / 2;
    const coordinate = Math.min(
      Math.max(label.desiredCoordinate, minimum),
      maximum,
    );

    return createLabel(label, coordinate);
  });

  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 validLabels = labels.filter((label) =>
    Number.isFinite(label.desiredCoordinate),
  );

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

  const reservedCoordinate = Math.min(
    Math.max(options.reservedCoordinate, 0),
    axisHeight,
  );

  const reservedHeight = Math.max(
    0,
    options.reservedHeight,
  );

  const reservedTop =
    reservedCoordinate - reservedHeight / 2;

  const reservedBottom =
    reservedCoordinate + reservedHeight / 2;

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

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

  const result = [
    ...layoutRange(
      labelsAbove,
      0,
      reservedTop - gap,
      gap,
      'end',
    ),
    ...layoutRange(
      labelsBelow,
      reservedBottom + gap,
      axisHeight,
      gap,
      'start',
    ),
  ];

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

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